Support delete for call links

Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
Jamie Kyle 2024-08-06 12:29:13 -07:00 committed by GitHub
parent 11fed7e7f8
commit 9a9f9495f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 853 additions and 345 deletions

View file

@ -7325,6 +7325,26 @@
"messageformat": "Share link via Signal",
"description": "Call History > Call Link Details > Share Link via Signal Button"
},
"icu:CallLinkDetails__DeleteLink": {
"messageformat": "Delete link",
"description": "Call History > Call Link Details > Delete Link Button"
},
"icu:CallLinkDetails__DeleteLinkModal__Title": {
"messageformat": "Delete call link?",
"description": "Call History > Call Link Details > Delete Link Modal > Title"
},
"icu:CallLinkDetails__DeleteLinkModal__Body": {
"messageformat": "This link will no longer work for anyone who has it.",
"description": "Call History > Call Link Details > Delete Link Modal > Body"
},
"icu:CallLinkDetails__DeleteLinkModal__Cancel": {
"messageformat": "Cancel",
"description": "Call History > Call Link Details > Delete Link Modal > Cancel Button"
},
"icu:CallLinkDetails__DeleteLinkModal__Delete": {
"messageformat": "Delete",
"description": "Call History > Call Link Details > Delete Link Modal > Delete Button"
},
"icu:CallLinkEditModal__Title": {
"messageformat": "Call link details",
"description": "Call Link Edit Modal > Title"

View file

@ -45,3 +45,17 @@
.CallLinkDetails__HeaderButton {
font-weight: 600;
}
.CallLinkDetails__DeleteLink {
// Override the default icon color
.ConversationDetails-icon__icon--trash::after {
@include any-theme {
background-color: $color-accent-red;
}
}
// Override the default label color
.ConversationDetails-panel-row__label {
color: $color-accent-red;
}
}

View file

@ -199,7 +199,7 @@ import { deriveStorageServiceKey } from './Crypto';
import { getThemeType } from './util/getThemeType';
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
import { CallMode } from './types/Calling';
import { CallMode } from './types/CallDisposition';
import type { SyncTaskType } from './util/syncTasks';
import { queueSyncTasks } from './util/syncTasks';
import type { ViewSyncTaskType } from './messageModifiers/ViewSyncs';

View file

@ -0,0 +1,40 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import type { ComponentMeta } from '../storybook/types';
import type { CallLinkDetailsProps } from './CallLinkDetails';
import { CallLinkDetails } from './CallLinkDetails';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
import {
FAKE_CALL_LINK,
FAKE_CALL_LINK_WITH_ADMIN_KEY,
} from '../test-both/helpers/fakeCallLink';
import { getFakeCallLinkHistoryGroup } from '../test-both/helpers/getFakeCallHistoryGroup';
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/CallLinkDetails',
component: CallLinkDetails,
argTypes: {},
args: {
i18n,
callHistoryGroup: getFakeCallLinkHistoryGroup(),
callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY,
onDeleteCallLink: action('onDeleteCallLink'),
onOpenCallLinkAddNameModal: action('onOpenCallLinkAddNameModal'),
onStartCallLinkLobby: action('onStartCallLinkLobby'),
onShareCallLinkViaSignal: action('onShareCallLinkViaSignal'),
onUpdateCallLinkRestrictions: action('onUpdateCallLinkRestrictions'),
},
} satisfies ComponentMeta<CallLinkDetailsProps>;
export function Admin(args: CallLinkDetailsProps): JSX.Element {
return <CallLinkDetails {...args} />;
}
export function NonAdmin(args: CallLinkDetailsProps): JSX.Element {
return <CallLinkDetails {...args} callLink={FAKE_CALL_LINK} />;
}

View file

@ -1,6 +1,6 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import React, { useState } from 'react';
import type { CallHistoryGroup } from '../types/CallDisposition';
import type { LocalizerType } from '../types/I18N';
import { CallHistoryGroupPanelSection } from './conversation/conversation-details/CallHistoryGroupPanelSection';
@ -17,8 +17,9 @@ import { Avatar, AvatarSize } from './Avatar';
import { Button, ButtonSize, ButtonVariant } from './Button';
import { copyCallLink } from '../util/copyLinksWithToast';
import { getColorForCallLink } from '../util/getColorForCallLink';
import { isCallLinkAdmin } from '../util/callLinks';
import { isCallLinkAdmin } from '../types/CallLink';
import { CallLinkRestrictionsSelect } from './CallLinkRestrictionsSelect';
import { ConfirmationDialog } from './ConfirmationDialog';
function toUrlWithoutProtocol(url: URL): string {
return `${url.hostname}${url.pathname}${url.search}${url.hash}`;
@ -28,6 +29,7 @@ export type CallLinkDetailsProps = Readonly<{
callHistoryGroup: CallHistoryGroup;
callLink: CallLinkType;
i18n: LocalizerType;
onDeleteCallLink: () => void;
onOpenCallLinkAddNameModal: () => void;
onStartCallLinkLobby: () => void;
onShareCallLinkViaSignal: () => void;
@ -38,11 +40,14 @@ export function CallLinkDetails({
callHistoryGroup,
callLink,
i18n,
onDeleteCallLink,
onOpenCallLinkAddNameModal,
onStartCallLinkLobby,
onShareCallLinkViaSignal,
onUpdateCallLinkRestrictions,
}: CallLinkDetailsProps): JSX.Element {
const [isDeleteCallLinkModalOpen, setIsDeleteCallLinkModalOpen] =
useState(false);
const webUrl = linkCallRoute.toWebUrl({
key: callLink.rootKey,
});
@ -144,6 +149,43 @@ export function CallLinkDetails({
onClick={onShareCallLinkViaSignal}
/>
</PanelSection>
{isCallLinkAdmin(callLink) && (
<PanelSection>
<PanelRow
className="CallLinkDetails__DeleteLink"
icon={
<ConversationDetailsIcon
ariaLabel={i18n('icu:CallLinkDetails__DeleteLink')}
icon={IconType.trash}
/>
}
label={i18n('icu:CallLinkDetails__DeleteLink')}
onClick={() => {
setIsDeleteCallLinkModalOpen(true);
}}
/>
</PanelSection>
)}
{isDeleteCallLinkModalOpen && (
<ConfirmationDialog
i18n={i18n}
dialogName="CallLinkDetails__DeleteLinkModal"
title={i18n('icu:CallLinkDetails__DeleteLinkModal__Title')}
cancelText={i18n('icu:CallLinkDetails__DeleteLinkModal__Cancel')}
actions={[
{
text: i18n('icu:CallLinkDetails__DeleteLinkModal__Delete'),
style: 'affirmative',
action: onDeleteCallLink,
},
]}
onClose={() => {
setIsDeleteCallLinkModalOpen(false);
}}
>
{i18n('icu:CallLinkDetails__DeleteLinkModal__Body')}
</ConfirmationDialog>
)}
</div>
);
}

View file

@ -8,12 +8,12 @@ import type { PropsType } from './CallManager';
import { CallManager } from './CallManager';
import {
CallEndedReason,
CallMode,
CallState,
CallViewMode,
GroupCallConnectionState,
GroupCallJoinState,
} from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import type {
ActiveGroupCallType,
GroupCallRemoteParticipantType,

View file

@ -20,11 +20,11 @@ import type {
} from '../types/Calling';
import {
CallEndedReason,
CallMode,
CallState,
GroupCallConnectionState,
GroupCallJoinState,
} from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import type { ConversationType } from '../state/ducks/conversations';
import type {
AcceptCallType,

View file

@ -3,7 +3,7 @@
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { CallMode } from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
export type PropsType = {
callMode: CallMode;

View file

@ -12,12 +12,12 @@ import type {
GroupCallRemoteParticipantType,
} from '../types/Calling';
import {
CallMode,
CallViewMode,
CallState,
GroupCallConnectionState,
GroupCallJoinState,
} from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import { generateAci } from '../types/ServiceId';
import type { ConversationType } from '../state/ducks/conversations';
import { AvatarColors } from '../types/Colors';

View file

@ -33,12 +33,12 @@ import type {
} from '../types/Calling';
import {
CALLING_REACTIONS_LIFETIME,
CallMode,
CallViewMode,
CallState,
GroupCallConnectionState,
GroupCallJoinState,
} from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import type { ServiceIdString } from '../types/ServiceId';
import { AvatarColors } from '../types/Colors';
import type { ConversationType } from '../state/ducks/conversations';

View file

@ -19,7 +19,7 @@ import {
getDefaultConversationWithServiceId,
} from '../test-both/helpers/getDefaultConversation';
import { CallingToastProvider } from './CallingToast';
import { CallMode } from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import { getDefaultCallLinkConversation } from '../test-both/helpers/fakeCallLink';
const i18n = setupI18n('en', enMessages);

View file

@ -19,7 +19,7 @@ import {
CallingLobbyJoinButton,
CallingLobbyJoinButtonVariant,
} from './CallingLobbyJoinButton';
import { CallMode } from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import type { CallingConversationType } from '../types/Calling';
import type { LocalizerType } from '../types/Util';
import { useIsOnline } from '../hooks/useIsOnline';

View file

@ -11,12 +11,12 @@ import type { PropsType } from './CallingPip';
import { CallingPip } from './CallingPip';
import type { ActiveDirectCallType } from '../types/Calling';
import {
CallMode,
CallViewMode,
CallState,
GroupCallConnectionState,
GroupCallJoinState,
} from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource';
import { setupI18n } from '../util/setupI18n';

View file

@ -14,7 +14,8 @@ import type {
GroupCallRemoteParticipantType,
GroupCallVideoRequest,
} from '../types/Calling';
import { CallMode, GroupCallJoinState } from '../types/Calling';
import { GroupCallJoinState } from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import { AvatarColors } from '../types/Colors';
import type { SetRendererCanvasType } from '../state/ducks/calling';
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';

View file

@ -3,7 +3,7 @@
import React, { useEffect, useMemo, useRef } from 'react';
import type { ActiveCallType } from '../types/Calling';
import { CallMode } from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import type { ConversationType } from '../state/ducks/conversations';
import type { LocalizerType } from '../types/Util';
import { CallingToastProvider, useCallingToasts } from './CallingToast';

View file

@ -28,6 +28,7 @@ import {
DirectCallStatus,
GroupCallStatus,
isSameCallHistoryGroup,
CallMode,
} from '../types/CallDisposition';
import { formatDateTimeShort, isMoreRecentThan } from '../util/timestamp';
import type { ConversationType } from '../state/ducks/conversations';
@ -47,7 +48,6 @@ import { CallsNewCallButton } from './CallsNewCall';
import { Tooltip, TooltipPlacement } from './Tooltip';
import { Theme } from '../util/theme';
import type { CallingConversationType } from '../types/Calling';
import { CallMode } from '../types/Calling';
import type { CallLinkType } from '../types/CallLink';
import {
callLinkToConversation,

View file

@ -62,7 +62,8 @@ type CallsTabProps = Readonly<{
preferredLeftPaneWidth: number;
renderCallLinkDetails: (
roomId: string,
callHistoryGroup: CallHistoryGroup
callHistoryGroup: CallHistoryGroup,
onClose: () => void
) => JSX.Element;
renderConversationDetails: (
conversationId: string,
@ -152,6 +153,10 @@ export function CallsTab({
[updateSelectedView]
);
const onCloseSelectedView = useCallback(() => {
updateSelectedView(null);
}, [updateSelectedView]);
useEscapeHandling(
sidebarView === CallsTabSidebarView.NewCallView
? () => {
@ -328,7 +333,8 @@ export function CallsTab({
{selectedView.type === 'callLink' &&
renderCallLinkDetails(
selectedView.roomId,
selectedView.callHistoryGroup
selectedView.callHistoryGroup,
onCloseSelectedView
)}
</div>
)}

View file

@ -6,7 +6,7 @@ import { action } from '@storybook/addon-actions';
import type { Meta } from '@storybook/react';
import type { PropsType } from './IncomingCallBar';
import { IncomingCallBar } from './IncomingCallBar';
import { CallMode } from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';

View file

@ -11,7 +11,7 @@ import { getParticipantName } from '../util/callingGetParticipantName';
import { ContactName } from './conversation/ContactName';
import type { LocalizerType } from '../types/Util';
import { AvatarColors } from '../types/Colors';
import { CallMode } from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import type { ConversationType } from '../state/ducks/conversations';
import type { AcceptCallType, DeclineCallType } from '../state/ducks/calling';
import { missingCaseError } from '../util/missingCaseError';

View file

@ -6,7 +6,13 @@ import { action } from '@storybook/addon-actions';
import type { Meta } from '@storybook/react';
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import { CallMode } from '../../types/Calling';
import {
CallMode,
CallType,
CallDirection,
GroupCallStatus,
DirectCallStatus,
} from '../../types/CallDisposition';
import { generateAci } from '../../types/ServiceId';
import { CallingNotification, type PropsType } from './CallingNotification';
import {
@ -14,12 +20,6 @@ import {
getDefaultGroup,
} from '../../test-both/helpers/getDefaultConversation';
import type { CallStatus } from '../../types/CallDisposition';
import {
CallType,
CallDirection,
GroupCallStatus,
DirectCallStatus,
} from '../../types/CallDisposition';
import type { ConversationType } from '../../state/ducks/conversations';
const i18n = setupI18n('en', enMessages);

View file

@ -10,7 +10,13 @@ import { SystemMessage, SystemMessageKind } from './SystemMessage';
import { Button, ButtonSize, ButtonVariant } from '../Button';
import { MessageTimestamp } from './MessageTimestamp';
import type { LocalizerType } from '../../types/Util';
import { CallMode } from '../../types/Calling';
import {
CallMode,
CallDirection,
CallType,
DirectCallStatus,
GroupCallStatus,
} from '../../types/CallDisposition';
import type { CallingNotificationType } from '../../util/callingNotification';
import {
getCallingIcon,
@ -19,12 +25,6 @@ import {
import { missingCaseError } from '../../util/missingCaseError';
import { Tooltip, TooltipPlacement } from '../Tooltip';
import * as log from '../../logging/log';
import {
CallDirection,
CallType,
DirectCallStatus,
GroupCallStatus,
} from '../../types/CallDisposition';
import {
type ContextMenuTriggerType,
MessageContextMenu,

View file

@ -11,7 +11,7 @@ import enMessages from '../../../_locales/en/messages.json';
import type { PropsType as TimelineItemProps } from './TimelineItem';
import { TimelineItem } from './TimelineItem';
import { UniversalTimerNotification } from './UniversalTimerNotification';
import { CallMode } from '../../types/Calling';
import { CallMode } from '../../types/CallDisposition';
import { AvatarColors } from '../../types/Colors';
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
import { WidthBreakpoint } from '../_util';

View file

@ -19,12 +19,7 @@ import { makeFakeLookupConversationWithoutServiceId } from '../../../test-both/h
import { ThemeType } from '../../../types/Util';
import { DurationInSeconds } from '../../../util/durations';
import { NavTab } from '../../../state/ducks/nav';
import { CallMode } from '../../../types/Calling';
import {
CallDirection,
CallType,
DirectCallStatus,
} from '../../../types/CallDisposition';
import { getFakeCallHistoryGroup } from '../../../test-both/helpers/getFakeCallHistoryGroup';
const i18n = setupI18n('en', enMessages);
@ -224,30 +219,15 @@ export const _11 = (): JSX.Element => (
<ConversationDetails {...createProps()} isGroup={false} />
);
function mins(n: number) {
return DurationInSeconds.toMillis(DurationInSeconds.fromMinutes(n));
}
export function WithCallHistoryGroup(): JSX.Element {
const props = createProps();
return (
<ConversationDetails
{...props}
callHistoryGroup={{
peerId: props.conversation?.serviceId ?? '',
mode: CallMode.Direct,
type: CallType.Video,
direction: CallDirection.Incoming,
status: DirectCallStatus.Accepted,
timestamp: Date.now(),
children: [
{ callId: '123', timestamp: Date.now() },
{ callId: '122', timestamp: Date.now() - mins(30) },
{ callId: '121', timestamp: Date.now() - mins(45) },
{ callId: '121', timestamp: Date.now() - mins(60) },
],
}}
callHistoryGroup={getFakeCallHistoryGroup({
peerId: props.conversation?.serviceId,
})}
selectedNavTab={NavTab.Calls}
/>
);

View file

@ -0,0 +1,70 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { z } from 'zod';
import type { LoggerType } from '../types/Logging';
import { DataReader, DataWriter } from '../sql/Client';
import type { JOB_STATUS } from './JobQueue';
import { JobQueue } from './JobQueue';
import { jobQueueDatabaseStore } from './JobQueueDatabaseStore';
import { calling } from '../services/calling';
import { toLogFormat } from '../types/errors';
const callLinksDeleteJobData = z.object({
source: z.string(),
});
type CallLinksDeleteJobData = z.infer<typeof callLinksDeleteJobData>;
export class CallLinksDeleteJobQueue extends JobQueue<CallLinksDeleteJobData> {
protected parseData(data: unknown): CallLinksDeleteJobData {
return callLinksDeleteJobData.parse(data);
}
protected async run(
{ data }: { data: CallLinksDeleteJobData },
{ attempt, log }: { attempt: number; log: LoggerType }
): Promise<typeof JOB_STATUS.NEEDS_RETRY | undefined> {
const { source } = data;
const logId = `callLinksDeleteJobQueue(${source}, attempt=${attempt})`;
const deletedCallLinks = await DataReader.getAllMarkedDeletedCallLinks();
if (deletedCallLinks.length === 0) {
log.info(`${logId}: no call links to delete`);
return undefined;
}
log.info(`${logId}: deleting ${deletedCallLinks.length} call links`);
const errors: Array<unknown> = [];
await Promise.all(
deletedCallLinks.map(async deletedCallLink => {
try {
// May 200 or 404 and that's fine
// Sends a CallLinkUpdate with type set to DELETE
await calling.deleteCallLink(deletedCallLink);
await DataWriter.finalizeDeleteCallLink(deletedCallLink.roomId);
log.info(`${logId}: deleted call link ${deletedCallLink.roomId}`);
} catch (error) {
log.error(
`${logId}: failed to delete call link ${deletedCallLink.roomId}`,
toLogFormat(error)
);
errors.push(error);
}
})
);
log.info(
`${logId}: Deleted ${deletedCallLinks.length} call links, failed to delete ${errors.length} call links`
);
if (errors.length > 0) {
throw new AggregateError(
errors,
`Failed to delete ${errors.length} call links`
);
}
return undefined;
}
}
export const callLinksDeleteJobQueue = new CallLinksDeleteJobQueue({
store: jobQueueDatabaseStore,
queueType: 'callLinksDelete',
maxAttempts: 25,
});

View file

@ -3,6 +3,7 @@
import type { WebAPIType } from '../textsecure/WebAPI';
import { drop } from '../util/drop';
import { callLinksDeleteJobQueue } from './callLinksDeleteJobQueue';
import { conversationJobQueue } from './conversationJobQueue';
import { groupAvatarJobQueue } from './groupAvatarJobQueue';
@ -40,6 +41,7 @@ export function initializeAllJobQueues({
// Other queues
drop(removeStorageKeyJobQueue.streamJobs());
drop(reportSpamJobQueue.streamJobs());
drop(callLinksDeleteJobQueue.streamJobs());
}
export async function shutdownAllJobQueues(): Promise<void> {
@ -52,5 +54,6 @@ export async function shutdownAllJobQueues(): Promise<void> {
viewOnceOpenJobQueue.shutdown(),
removeStorageKeyJobQueue.shutdown(),
reportSpamJobQueue.shutdown(),
callLinksDeleteJobQueue.shutdown(),
]);
}

View file

@ -68,11 +68,11 @@ import type {
PresentedSource,
} from '../types/Calling';
import {
CallMode,
GroupCallConnectionState,
GroupCallJoinState,
ScreenShareStatus,
} from '../types/Calling';
import { CallMode, LocalCallEvent } from '../types/CallDisposition';
import {
findBestMatchingAudioDeviceIndex,
findBestMatchingCameraId,
@ -135,17 +135,16 @@ import {
getCallDetailsForAdhocCall,
} from '../util/callDisposition';
import { isNormalNumber } from '../util/isNormalNumber';
import { LocalCallEvent } from '../types/CallDisposition';
import type { AciString, ServiceIdString } from '../types/ServiceId';
import { isServiceIdString } from '../types/ServiceId';
import { isInSystemContacts } from '../util/isInSystemContacts';
import { toAdminKeyBytes } from '../util/callLinks';
import {
getRoomIdFromRootKey,
getCallLinkAuthCredentialPresentation,
toAdminKeyBytes,
getRoomIdFromRootKey,
callLinkRestrictionsToRingRTC,
callLinkStateFromRingRTC,
} from '../util/callLinks';
} from '../util/callLinksRingrtc';
import { isAdhocCallingEnabled } from '../util/isAdhocCallingEnabled';
import {
conversationJobQueue,
@ -154,7 +153,10 @@ import {
import type { CallLinkType, CallLinkStateType } from '../types/CallLink';
import { CallLinkRestrictions } from '../types/CallLink';
import { getConversationIdForLogging } from '../util/idForLogging';
import { sendCallLinkUpdateSync } from '../util/sendCallLinkUpdateSync';
import {
sendCallLinkDeleteSync,
sendCallLinkUpdateSync,
} from '../util/sendCallLinkUpdateSync';
import { createIdenticon } from '../util/createIdenticon';
import { getColorForCallLink } from '../util/getColorForCallLink';
@ -683,6 +685,41 @@ export class CallingClass {
return callLink;
}
async deleteCallLink(callLink: CallLinkType): Promise<void> {
strictAssert(
this._sfuUrl,
'createCallLink() missing SFU URL; not deleting call link'
);
const sfuUrl = this._sfuUrl;
const logId = `deleteCallLink(${callLink.roomId})`;
const callLinkRootKey = CallLinkRootKey.parse(callLink.rootKey);
strictAssert(callLink.adminKey, 'Missing admin key');
const callLinkAdminKey = toAdminKeyBytes(callLink.adminKey);
const authCredentialPresentation =
await getCallLinkAuthCredentialPresentation(callLinkRootKey);
const result = await RingRTC.deleteCallLink(
sfuUrl,
authCredentialPresentation.serialize(),
callLinkRootKey,
callLinkAdminKey
);
if (!result.success) {
if (result.errorStatusCode === 404) {
log.info(`${logId}: Call link not found, already deleted`);
return;
}
const message = `Failed to delete call link: ${result.errorStatusCode}`;
log.error(`${logId}: ${message}`);
throw new Error(message);
}
drop(sendCallLinkDeleteSync(callLink));
}
async updateCallLinkName(
callLink: CallLinkType,
name: string

View file

@ -562,6 +562,7 @@ type ReadableInterface = {
callLinkExists(roomId: string): boolean;
getAllCallLinks: () => ReadonlyArray<CallLinkType>;
getCallLinkByRoomId: (roomId: string) => CallLinkType | undefined;
getAllMarkedDeletedCallLinks(): ReadonlyArray<CallLinkType>;
getMessagesBetween: (
conversationId: string,
options: GetMessagesBetweenOptions
@ -794,6 +795,10 @@ type WritableInterface = {
roomId: string,
callLinkState: CallLinkStateType
): CallLinkType;
beginDeleteAllCallLinks(): void;
beginDeleteCallLink(roomId: string): void;
finalizeDeleteCallLink(roomId: string): void;
deleteCallLinkFromSync(roomId: string): void;
migrateConversationMessages: (obsoleteId: string, currentId: string) => void;
saveEditedMessage: (
mainMessage: ReadonlyDeep<MessageType>,

View file

@ -168,6 +168,7 @@ import {
GroupCallStatus,
CallType,
CallStatusValue,
CallMode,
} from '../types/CallDisposition';
import {
callLinkExists,
@ -176,13 +177,17 @@ import {
insertCallLink,
updateCallLinkAdminKeyByRoomId,
updateCallLinkState,
beginDeleteAllCallLinks,
getAllMarkedDeletedCallLinks,
finalizeDeleteCallLink,
beginDeleteCallLink,
deleteCallLinkFromSync,
} from './server/callLinks';
import {
replaceAllEndorsementsForGroup,
deleteAllEndorsementsForGroup,
getGroupSendCombinedEndorsementExpiration,
} from './server/groupEndorsements';
import { CallMode } from '../types/Calling';
import {
attachmentDownloadJobSchema,
type AttachmentDownloadJobType,
@ -296,6 +301,7 @@ export const DataReader: ServerReadableInterface = {
callLinkExists,
getAllCallLinks,
getCallLinkByRoomId,
getAllMarkedDeletedCallLinks,
getMessagesBetween,
getNearbyMessageFromDeletedSet,
getMostRecentAddressableMessages,
@ -430,6 +436,10 @@ export const DataWriter: ServerWritableInterface = {
insertCallLink,
updateCallLinkAdminKeyByRoomId,
updateCallLinkState,
beginDeleteAllCallLinks,
beginDeleteCallLink,
finalizeDeleteCallLink,
deleteCallLinkFromSync,
migrateConversationMessages,
saveEditedMessage,
saveEditedMessages,
@ -3458,7 +3468,7 @@ function clearCallHistory(
return db.transaction(() => {
const timestamp = getMessageTimestampForCallLogEventTarget(db, target);
const [selectCallIdsQuery, selectCallIdsParams] = sql`
const [selectCallsQuery, selectCallsParams] = sql`
SELECT callsHistory.callId
FROM callsHistory
WHERE
@ -3471,18 +3481,30 @@ function clearCallHistory(
);
`;
const callIds = db
.prepare(selectCallIdsQuery)
const deletedCallIds: ReadonlyArray<string> = db
.prepare(selectCallsQuery)
.pluck()
.all(selectCallIdsParams);
.all(selectCallsParams);
let deletedMessageIds: ReadonlyArray<string> = [];
batchMultiVarQuery(db, callIds, (ids: ReadonlyArray<string>): void => {
batchMultiVarQuery(db, deletedCallIds, (ids): void => {
const idsFragment = sqlJoin(ids);
const [clearCallsHistoryQuery, clearCallsHistoryParams] = sql`
UPDATE callsHistory
SET
status = ${DirectCallStatus.Deleted},
timestamp = ${Date.now()}
WHERE callsHistory.callId IN (${idsFragment});
`;
db.prepare(clearCallsHistoryQuery).run(clearCallsHistoryParams);
const [deleteMessagesQuery, deleteMessagesParams] = sql`
DELETE FROM messages
WHERE messages.type IS 'call-history'
AND messages.callId IN (${sqlJoin(ids)})
AND messages.callId IN (${idsFragment})
RETURNING id;
`;
@ -3494,21 +3516,6 @@ function clearCallHistory(
deletedMessageIds = deletedMessageIds.concat(batchDeletedMessageIds);
});
const [clearCallsHistoryQuery, clearCallsHistoryParams] = sql`
UPDATE callsHistory
SET
status = ${DirectCallStatus.Deleted},
timestamp = ${Date.now()}
WHERE callsHistory.timestamp <= ${timestamp};
`;
try {
db.prepare(clearCallsHistoryQuery).run(clearCallsHistoryParams);
} catch (error) {
logger.error(error, error.message);
throw error;
}
return deletedMessageIds;
})();
}
@ -3519,9 +3526,8 @@ function markCallHistoryDeleted(db: WritableDB, callId: string): void {
SET
status = ${DirectCallStatus.Deleted},
timestamp = ${Date.now()}
WHERE callId = ${callId};
WHERE callId = ${callId}
`;
db.prepare(query).run(params);
}
@ -4829,7 +4835,7 @@ function getNextAttachmentBackupJobs(
active = 0
AND
(retryAfter is NULL OR retryAfter <= ${timestamp})
ORDER BY
ORDER BY
-- type is "standard" or "thumbnail"; we prefer "standard" jobs
type ASC, receivedAt DESC
LIMIT ${limit}

View file

@ -0,0 +1,31 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from '@signalapp/better-sqlite3';
import type { LoggerType } from '../../types/Logging';
export const version = 1140;
export function updateToSchemaVersion1140(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 1140) {
return;
}
db.transaction(() => {
db.exec(`
DROP INDEX IF EXISTS callLinks_deleted;
ALTER TABLE callLinks
ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;
CREATE INDEX callLinks_deleted
ON callLinks (deleted, roomId);
`);
})();
db.pragma('user_version = 1140');
logger.info('updateToSchemaVersion1140: success!');
}

View file

@ -17,8 +17,8 @@ import {
CallType,
GroupCallStatus,
callHistoryDetailsSchema,
CallMode,
} from '../../types/CallDisposition';
import { CallMode } from '../../types/Calling';
import type { WritableDB, MessageType, ConversationType } from '../Interface';
import { strictAssert } from '../../util/assert';
import { missingCaseError } from '../../util/missingCaseError';

View file

@ -89,10 +89,11 @@ import { updateToSchemaVersion1090 } from './1090-message-delete-indexes';
import { updateToSchemaVersion1100 } from './1100-optimize-mark-call-history-read-in-conversation';
import { updateToSchemaVersion1110 } from './1110-sticker-local-key';
import { updateToSchemaVersion1120 } from './1120-messages-foreign-keys-indexes';
import { updateToSchemaVersion1130 } from './1130-isStory-index';
import {
updateToSchemaVersion1130,
updateToSchemaVersion1140,
version as MAX_VERSION,
} from './1130-isStory-index';
} from './1140-call-links-deleted-column';
function updateToSchemaVersion1(
currentVersion: number,
@ -2050,6 +2051,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion1110,
updateToSchemaVersion1120,
updateToSchemaVersion1130,
updateToSchemaVersion1140,
];
export class DBVersionFromFutureError extends Error {

View file

@ -7,15 +7,16 @@ import {
callLinkRestrictionsSchema,
callLinkRecordSchema,
} from '../../types/CallLink';
import { toAdminKeyBytes } from '../../util/callLinks';
import {
callLinkToRecord,
callLinkFromRecord,
toAdminKeyBytes,
} from '../../util/callLinks';
} from '../../util/callLinksRingrtc';
import type { ReadableDB, WritableDB } from '../Interface';
import { prepare } from '../Server';
import { sql } from '../util';
import { strictAssert } from '../../util/assert';
import { CallStatusValue } from '../../types/CallDisposition';
export function callLinkExists(db: ReadableDB, roomId: string): boolean {
const [query, params] = sql`
@ -133,3 +134,106 @@ function assertRoomIdMatchesRootKey(roomId: string, rootKey: string): void {
'passed roomId must match roomId derived from root key'
);
}
function deleteCallHistoryByRoomId(db: WritableDB, roomId: string) {
const [
markCallHistoryDeleteByPeerIdQuery,
markCallHistoryDeleteByPeerIdParams,
] = sql`
UPDATE callsHistory
SET
status = ${CallStatusValue.Deleted},
timestamp = ${Date.now()}
WHERE peerId = ${roomId}
`;
db.prepare(markCallHistoryDeleteByPeerIdQuery).run(
markCallHistoryDeleteByPeerIdParams
);
}
// This should only be called from a sync message to avoid accidentally deleting
// on the client but not the server
export function deleteCallLinkFromSync(db: WritableDB, roomId: string): void {
db.transaction(() => {
const [query, params] = sql`
DELETE FROM callLinks
WHERE roomId = ${roomId};
`;
db.prepare(query).run(params);
deleteCallHistoryByRoomId(db, roomId);
})();
}
export function beginDeleteCallLink(db: WritableDB, roomId: string): void {
db.transaction(() => {
// If adminKey is null, then we should delete the call link
const [deleteNonAdminCallLinksQuery, deleteNonAdminCallLinksParams] = sql`
DELETE FROM callLinks
WHERE adminKey IS NULL
AND roomId = ${roomId};
`;
const result = db
.prepare(deleteNonAdminCallLinksQuery)
.run(deleteNonAdminCallLinksParams);
// Skip this query if the call is already deleted
if (result.changes === 0) {
// If the admin key is not null, we should mark it for deletion
const [markAdminCallLinksDeletedQuery, markAdminCallLinksDeletedParams] =
sql`
UPDATE callLinks
SET deleted = 1
WHERE adminKey IS NOT NULL
AND roomId = ${roomId};
`;
db.prepare(markAdminCallLinksDeletedQuery).run(
markAdminCallLinksDeletedParams
);
}
deleteCallHistoryByRoomId(db, roomId);
})();
}
export function beginDeleteAllCallLinks(db: WritableDB): void {
db.transaction(() => {
const [markAdminCallLinksDeletedQuery] = sql`
UPDATE callLinks
SET deleted = 1
WHERE adminKey IS NOT NULL;
`;
db.prepare(markAdminCallLinksDeletedQuery).run();
const [deleteNonAdminCallLinksQuery] = sql`
DELETE FROM callLinks
WHERE adminKey IS NULL;
`;
db.prepare(deleteNonAdminCallLinksQuery).run();
})();
}
export function getAllMarkedDeletedCallLinks(
db: ReadableDB
): ReadonlyArray<CallLinkType> {
const [query] = sql`
SELECT * FROM callLinks WHERE deleted = 1;
`;
return db
.prepare(query)
.all()
.map(item => callLinkFromRecord(callLinkRecordSchema.parse(item)));
}
export function finalizeDeleteCallLink(db: WritableDB, roomId: string): void {
const [query, params] = sql`
DELETE FROM callLinks WHERE roomId = ${roomId} AND deleted = 1;
`;
db.prepare(query).run(params);
}

View file

@ -23,6 +23,12 @@ import {
getCallHistoryLatestCall,
getCallHistorySelector,
} from '../selectors/callHistory';
import {
getCallsHistoryForRedux,
getCallsHistoryUnreadCountForRedux,
loadCallsHistory,
} from '../../services/callHistoryLoader';
import { makeLookup } from '../../util/makeLookup';
export type CallHistoryState = ReadonlyDeep<{
// This informs the app that underlying call history data has changed.
@ -34,6 +40,7 @@ export type CallHistoryState = ReadonlyDeep<{
const CALL_HISTORY_ADD = 'callHistory/ADD';
const CALL_HISTORY_REMOVE = 'callHistory/REMOVE';
const CALL_HISTORY_RESET = 'callHistory/RESET';
const CALL_HISTORY_RELOAD = 'callHistory/RELOAD';
const CALL_HISTORY_UPDATE_UNREAD = 'callHistory/UPDATE_UNREAD';
export type CallHistoryAdd = ReadonlyDeep<{
@ -50,6 +57,14 @@ export type CallHistoryReset = ReadonlyDeep<{
type: typeof CALL_HISTORY_RESET;
}>;
export type CallHistoryReload = ReadonlyDeep<{
type: typeof CALL_HISTORY_RELOAD;
payload: {
callsHistory: ReadonlyArray<CallHistoryDetails>;
callsHistoryUnreadCount: number;
};
}>;
export type CallHistoryUpdateUnread = ReadonlyDeep<{
type: typeof CALL_HISTORY_UPDATE_UNREAD;
payload: number;
@ -59,6 +74,7 @@ export type CallHistoryAction = ReadonlyDeep<
| CallHistoryAdd
| CallHistoryRemove
| CallHistoryReset
| CallHistoryReload
| CallHistoryUpdateUnread
>;
@ -178,9 +194,29 @@ function clearAllCallHistory(): ThunkAction<
} catch (error) {
log.error('Error clearing call history', Errors.toLogFormat(error));
} finally {
// Just force a reset, even if the clear failed.
dispatch(resetCallHistory());
dispatch(updateCallHistoryUnreadCount());
// Just force a reload, even if the clear failed.
dispatch(reloadCallHistory());
}
};
}
export function reloadCallHistory(): ThunkAction<
void,
RootStateType,
unknown,
CallHistoryReload
> {
return async dispatch => {
try {
await loadCallsHistory();
const callsHistory = getCallsHistoryForRedux();
const callsHistoryUnreadCount = getCallsHistoryUnreadCountForRedux();
dispatch({
type: CALL_HISTORY_RELOAD,
payload: { callsHistory, callsHistoryUnreadCount },
});
} catch (error) {
log.error('Error reloading call history', Errors.toLogFormat(error));
}
};
}
@ -226,6 +262,12 @@ export function reducer(
...state,
unreadCount: action.payload,
};
case CALL_HISTORY_RELOAD:
return {
edition: state.edition + 1,
unreadCount: action.payload.callsHistoryUnreadCount,
callHistoryByCallId: makeLookup(action.payload.callsHistory, 'callId'),
};
default:
return state;
}

View file

@ -41,21 +41,21 @@ import {
MAX_CALLING_REACTIONS,
CallEndedReason,
CallingDeviceType,
CallMode,
CallViewMode,
CallState,
GroupCallConnectionState,
GroupCallJoinState,
} from '../../types/Calling';
import { CallMode } from '../../types/CallDisposition';
import { callingTones } from '../../util/callingTones';
import { requestCameraPermissions } from '../../util/callingPermissions';
import {
CALL_LINK_DEFAULT_STATE,
getRoomIdFromRootKey,
isCallLinksCreateEnabled,
toAdminKeyBytes,
toCallHistoryFromUnusedCallLink,
} from '../../util/callLinks';
import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc';
import { sendCallLinkUpdateSync } from '../../util/sendCallLinkUpdateSync';
import { sleep } from '../../util/sleep';
import { LatestQueue } from '../../util/LatestQueue';
@ -92,10 +92,11 @@ import { getConversationIdForLogging } from '../../util/idForLogging';
import { DataReader, DataWriter } from '../../sql/Client';
import { isAciString } from '../../util/isAciString';
import type { CallHistoryAdd } from './callHistory';
import { addCallHistory } from './callHistory';
import { addCallHistory, reloadCallHistory } from './callHistory';
import { saveDraftRecordingIfNeeded } from './composer';
import type { CallHistoryDetails } from '../../types/CallDisposition';
import type { StartCallData } from '../../components/ConfirmLeaveCallModal';
import { callLinksDeleteJobQueue } from '../../jobs/callLinksDeleteJobQueue';
import { getCallLinksByRoomId } from '../selectors/calling';
// State
@ -255,6 +256,10 @@ type HandleCallLinkUpdateActionPayloadType = ReadonlyDeep<{
callLink: CallLinkType;
}>;
type HandleCallLinkDeleteActionPayloadType = ReadonlyDeep<{
roomId: string;
}>;
type HangUpActionPayloadType = ReadonlyDeep<{
conversationId: string;
}>;
@ -264,6 +269,10 @@ export type HandleCallLinkUpdateType = ReadonlyDeep<{
adminKey: string | null;
}>;
export type HandleCallLinkDeleteType = ReadonlyDeep<{
roomId: string;
}>;
export type IncomingDirectCallType = ReadonlyDeep<{
conversationId: string;
isVideoCall: boolean;
@ -598,6 +607,7 @@ const GROUP_CALL_STATE_CHANGE = 'calling/GROUP_CALL_STATE_CHANGE';
const GROUP_CALL_REACTIONS_RECEIVED = 'calling/GROUP_CALL_REACTIONS_RECEIVED';
const GROUP_CALL_REACTIONS_EXPIRED = 'calling/GROUP_CALL_REACTIONS_EXPIRED';
const HANDLE_CALL_LINK_UPDATE = 'calling/HANDLE_CALL_LINK_UPDATE';
const HANDLE_CALL_LINK_DELETE = 'calling/HANDLE_CALL_LINK_DELETE';
const HANG_UP = 'calling/HANG_UP';
const INCOMING_DIRECT_CALL = 'calling/INCOMING_DIRECT_CALL';
const INCOMING_GROUP_CALL = 'calling/INCOMING_GROUP_CALL';
@ -740,10 +750,15 @@ type GroupCallReactionsExpiredActionType = ReadonlyDeep<{
}>;
type HandleCallLinkUpdateActionType = ReadonlyDeep<{
type: 'calling/HANDLE_CALL_LINK_UPDATE';
type: typeof HANDLE_CALL_LINK_UPDATE;
payload: HandleCallLinkUpdateActionPayloadType;
}>;
type HandleCallLinkDeleteActionType = ReadonlyDeep<{
type: typeof HANDLE_CALL_LINK_DELETE;
payload: HandleCallLinkDeleteActionPayloadType;
}>;
type HangUpActionType = ReadonlyDeep<{
type: 'calling/HANG_UP';
payload: HangUpActionPayloadType;
@ -903,6 +918,7 @@ export type CallingActionType =
| GroupCallReactionsReceivedActionType
| GroupCallReactionsExpiredActionType
| HandleCallLinkUpdateActionType
| HandleCallLinkDeleteActionType
| HangUpActionType
| IncomingDirectCallActionType
| IncomingGroupCallActionType
@ -1466,6 +1482,19 @@ function handleCallLinkUpdate(
};
}
function handleCallLinkDelete(
payload: HandleCallLinkDeleteType
): ThunkAction<void, RootStateType, unknown, HandleCallLinkDeleteActionType> {
return async dispatch => {
dispatch({
type: HANDLE_CALL_LINK_DELETE,
payload,
});
dispatch(reloadCallHistory());
};
}
function hangUpActiveCall(
reason: string
): ThunkAction<void, RootStateType, unknown, HangUpActionType> {
@ -1971,6 +2000,16 @@ function createCallLink(
};
}
function deleteCallLink(
roomId: string
): ThunkAction<void, RootStateType, unknown, HandleCallLinkDeleteActionType> {
return async dispatch => {
await DataWriter.beginDeleteCallLink(roomId);
await callLinksDeleteJobQueue.add({ source: 'deleteCallLink' });
dispatch(handleCallLinkDelete({ roomId }));
};
}
function updateCallLinkName(
roomId: string,
name: string
@ -2394,6 +2433,7 @@ export const actions = {
closeNeedPermissionScreen,
createCallLink,
declineCall,
deleteCallLink,
denyUser,
getPresentingSources,
groupCallAudioLevelsChange,
@ -2402,6 +2442,7 @@ export const actions = {
groupCallStateChange,
hangUpActiveCall,
handleCallLinkUpdate,
handleCallLinkDelete,
joinedAdhocCall,
leaveCurrentCallAndStartCallingLobby,
onOutgoingVideoCallInConversation,

View file

@ -3,11 +3,8 @@
// Note that this file should not important any binary addons or Node.js modules
// because it can be imported by storybook
import {
CallMode,
CallState,
GroupCallConnectionState,
} from '../../types/Calling';
import { CallState, GroupCallConnectionState } from '../../types/Calling';
import { CallMode } from '../../types/CallDisposition';
import type { AciString } from '../../types/ServiceId';
import { missingCaseError } from '../../util/missingCaseError';
import type {

View file

@ -70,7 +70,7 @@ import type {
DraftBodyRanges,
HydratedBodyRangesType,
} from '../../types/BodyRange';
import { CallMode } from '../../types/Calling';
import { CallMode } from '../../types/CallDisposition';
import type { MediaItemType } from '../../types/MediaItem';
import type { StoryDistributionIdString } from '../../types/StoryDistributionId';
import { normalizeStoryDistributionId } from '../../types/StoryDistributionId';

View file

@ -13,7 +13,7 @@ import type {
GroupCallStateType,
} from '../ducks/calling';
import { getIncomingCall as getIncomingCallHelper } from '../ducks/callingHelpers';
import { CallMode } from '../../types/Calling';
import { CallMode } from '../../types/CallDisposition';
import type { CallLinkType } from '../../types/CallLink';
import { getUserACI } from './user';
import { getOwn } from '../../util/getOwn';

View file

@ -144,8 +144,7 @@ import {
} from '../../util/getTitle';
import { getMessageSentTimestamp } from '../../util/getMessageSentTimestamp';
import type { CallHistorySelectorType } from './callHistory';
import { CallMode } from '../../types/Calling';
import { CallDirection } from '../../types/CallDisposition';
import { CallMode, CallDirection } from '../../types/CallDisposition';
import { getCallIdFromEra } from '../../util/callDisposition';
import { LONG_MESSAGE } from '../../types/MIME';
import type { MessageRequestResponseNotificationData } from '../../components/conversation/MessageRequestResponseNotification';

View file

@ -9,10 +9,8 @@ import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
import { getCallLinkAddNameModalRoomId } from '../selectors/globalModals';
import { strictAssert } from '../../util/assert';
import {
isCallLinkAdmin,
isCallLinksCreateEnabled,
} from '../../util/callLinks';
import { isCallLinksCreateEnabled } from '../../util/callLinks';
import { isCallLinkAdmin } from '../../types/CallLink';
import { CallLinkAddNameModal } from '../../components/CallLinkAddNameModal';
export const SmartCallLinkAddNameModal = memo(

View file

@ -15,21 +15,30 @@ import type { CallLinkRestrictions } from '../../types/CallLink';
export type SmartCallLinkDetailsProps = Readonly<{
roomId: string;
callHistoryGroup: CallHistoryGroup;
onClose: () => void;
}>;
export const SmartCallLinkDetails = memo(function SmartCallLinkDetails({
roomId,
callHistoryGroup,
onClose,
}: SmartCallLinkDetailsProps) {
const i18n = useSelector(getIntl);
const callLinkSelector = useSelector(getCallLinkSelector);
const { startCallLinkLobby, updateCallLinkRestrictions } =
const { deleteCallLink, startCallLinkLobby, updateCallLinkRestrictions } =
useCallingActions();
const { toggleCallLinkAddNameModal, showShareCallLinkViaSignal } =
useGlobalModalActions();
const callLink = callLinkSelector(roomId);
const handleDeleteCallLink = useCallback(() => {
strictAssert(callLink != null, 'callLink not found');
deleteCallLink(callLink.roomId);
onClose();
}, [callLink, deleteCallLink, onClose]);
const handleOpenCallLinkAddNameModal = useCallback(() => {
toggleCallLinkAddNameModal(roomId);
}, [roomId, toggleCallLinkAddNameModal]);
@ -61,6 +70,7 @@ export const SmartCallLinkDetails = memo(function SmartCallLinkDetails({
callHistoryGroup={callHistoryGroup}
callLink={callLink}
i18n={i18n}
onDeleteCallLink={handleDeleteCallLink}
onOpenCallLinkAddNameModal={handleOpenCallLinkAddNameModal}
onStartCallLinkLobby={handleStartCallLinkLobby}
onShareCallLinkViaSignal={handleShareCallLinkViaSignal}

View file

@ -32,7 +32,8 @@ import type {
ConversationsByDemuxIdType,
GroupCallRemoteParticipantType,
} from '../../types/Calling';
import { CallMode, CallState } from '../../types/Calling';
import { CallState } from '../../types/Calling';
import { CallMode } from '../../types/CallDisposition';
import type { AciString } from '../../types/ServiceId';
import { strictAssert } from '../../util/assert';
import { callLinkToConversation } from '../../util/callLinks';

View file

@ -107,10 +107,15 @@ function getCallHistoryFilter({
function renderCallLinkDetails(
roomId: string,
callHistoryGroup: CallHistoryGroup
callHistoryGroup: CallHistoryGroup,
onClose: () => void
): JSX.Element {
return (
<SmartCallLinkDetails roomId={roomId} callHistoryGroup={callHistoryGroup} />
<SmartCallLinkDetails
roomId={roomId}
callHistoryGroup={callHistoryGroup}
onClose={onClose}
/>
);
}
@ -167,11 +172,8 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
startCallLinkLobbyByRoomId,
togglePip,
} = useCallingActions();
const {
clearAllCallHistory: clearCallHistory,
markCallHistoryRead,
markCallsTabViewed,
} = useCallHistoryActions();
const { clearAllCallHistory, markCallHistoryRead, markCallsTabViewed } =
useCallHistoryActions();
const { toggleCallLinkEditModal, toggleConfirmLeaveCallModal } =
useGlobalModalActions();
@ -244,7 +246,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
hasPendingUpdate={hasPendingUpdate}
i18n={i18n}
navTabsCollapsed={navTabsCollapsed}
onClearCallHistory={clearCallHistory}
onClearCallHistory={clearAllCallHistory}
onMarkCallHistoryRead={markCallHistoryRead}
onToggleNavTabsCollapse={toggleNavTabsCollapse}
onCreateCallLink={handleCreateCallLink}

View file

@ -10,7 +10,7 @@ import {
} from '../../components/conversation/ConversationHeader';
import { getCannotLeaveBecauseYouAreLastAdmin } from '../../components/conversation/conversation-details/ConversationDetails';
import { useMinimalConversation } from '../../hooks/useMinimalConversation';
import { CallMode } from '../../types/Calling';
import { CallMode } from '../../types/CallDisposition';
import { PanelType } from '../../types/Panels';
import { StoryViewModeType } from '../../types/Stories';
import { strictAssert } from '../../util/assert';

View file

@ -0,0 +1,47 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { CallHistoryGroup } from '../../types/CallDisposition';
import {
AdhocCallStatus,
CallDirection,
CallType,
DirectCallStatus,
CallMode,
} from '../../types/CallDisposition';
import { DurationInSeconds } from '../../util/durations';
function mins(n: number) {
return DurationInSeconds.toMillis(DurationInSeconds.fromMinutes(n));
}
export function getFakeCallHistoryGroup(
overrides: Partial<CallHistoryGroup> = {}
): CallHistoryGroup {
return {
peerId: '',
mode: CallMode.Direct,
type: CallType.Video,
direction: CallDirection.Incoming,
status: DirectCallStatus.Accepted,
timestamp: Date.now(),
children: [
{ callId: '123', timestamp: Date.now() },
{ callId: '122', timestamp: Date.now() - mins(30) },
{ callId: '121', timestamp: Date.now() - mins(45) },
{ callId: '121', timestamp: Date.now() - mins(60) },
],
...overrides,
};
}
export function getFakeCallLinkHistoryGroup(
overrides: Partial<CallHistoryGroup> = {}
): CallHistoryGroup {
return getFakeCallHistoryGroup({
mode: CallMode.Adhoc,
type: CallType.Adhoc,
status: AdhocCallStatus.Joined,
...overrides,
});
}

View file

@ -3,7 +3,10 @@
import { assert } from 'chai';
import { callLinkToRecord, callLinkFromRecord } from '../../util/callLinks';
import {
callLinkToRecord,
callLinkFromRecord,
} from '../../util/callLinksRingrtc';
import {
FAKE_CALL_LINK as CALL_LINK,
FAKE_CALL_LINK_WITH_ADMIN_KEY as CALL_LINK_WITH_ADMIN_KEY,

View file

@ -3,18 +3,18 @@
import { assert } from 'chai';
import { getCallingNotificationText } from '../../util/callingNotification';
import { CallMode } from '../../types/Calling';
import {
CallMode,
CallDirection,
CallType,
GroupCallStatus,
} from '../../types/CallDisposition';
import { setupI18n } from '../../util/setupI18n';
import enMessages from '../../../_locales/en/messages.json';
import {
getDefaultConversation,
getDefaultGroup,
} from '../helpers/getDefaultConversation';
import {
CallDirection,
CallType,
GroupCallStatus,
} from '../../types/CallDisposition';
import { getPeerIdFromConversation } from '../../util/callDisposition';
import { HOUR } from '../../util/durations';

View file

@ -6,7 +6,14 @@ import { v4 as generateUuid } from 'uuid';
import { DataReader, DataWriter } from '../../sql/Client';
import { CallMode } from '../../types/Calling';
import {
CallMode,
AdhocCallStatus,
CallDirection,
CallHistoryFilterStatus,
CallType,
DirectCallStatus,
} from '../../types/CallDisposition';
import { generateAci } from '../../types/ServiceId';
import type { ServiceIdString } from '../../types/ServiceId';
import type {
@ -14,13 +21,6 @@ import type {
CallHistoryGroup,
CallStatus,
} from '../../types/CallDisposition';
import {
AdhocCallStatus,
CallDirection,
CallHistoryFilterStatus,
CallType,
DirectCallStatus,
} from '../../types/CallDisposition';
import { strictAssert } from '../../util/assert';
import type { ConversationAttributesType } from '../../model-types';
import {

View file

@ -7,14 +7,14 @@ import { v4 as generateUuid } from 'uuid';
import { times } from 'lodash';
import { DataReader, DataWriter } from '../../sql/Client';
import { CallMode } from '../../types/Calling';
import { generateAci } from '../../types/ServiceId';
import type { CallHistoryDetails } from '../../types/CallDisposition';
import {
CallMode,
CallDirection,
CallType,
GroupCallStatus,
} from '../../types/CallDisposition';
import { generateAci } from '../../types/ServiceId';
import type { CallHistoryDetails } from '../../types/CallDisposition';
import type { MaybeStaleCallHistory } from '../../sql/Server';
const { getAllCallHistory } = DataReader;

View file

@ -30,12 +30,12 @@ import { isAnybodyElseInGroupCall } from '../../../state/ducks/callingHelpers';
import { truncateAudioLevel } from '../../../calling/truncateAudioLevel';
import { calling as callingService } from '../../../services/calling';
import {
CallMode,
CallState,
CallViewMode,
GroupCallConnectionState,
GroupCallJoinState,
} from '../../../types/Calling';
import { CallMode } from '../../../types/CallDisposition';
import { generateAci } from '../../../types/ServiceId';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
import type { UnwrapPromise } from '../../../types/Util';

View file

@ -36,7 +36,7 @@ import {
} from '../../../state/ducks/conversations';
import { ReadStatus } from '../../../messages/MessageReadStatus';
import type { SingleServePromiseIdString } from '../../../services/singleServePromise';
import { CallMode } from '../../../types/Calling';
import { CallMode } from '../../../types/CallDisposition';
import { generateAci, getAciFromPrefix } from '../../../types/ServiceId';
import { generateStoryDistributionId } from '../../../types/StoryDistributionId';
import {

View file

@ -6,12 +6,12 @@ import { reducer as rootReducer } from '../../../state/reducer';
import { noopAction } from '../../../state/ducks/noop';
import { actions as userActions } from '../../../state/ducks/user';
import {
CallMode,
CallState,
CallViewMode,
GroupCallConnectionState,
GroupCallJoinState,
} from '../../../types/Calling';
import { CallMode } from '../../../types/CallDisposition';
import { generateAci } from '../../../types/ServiceId';
import {
getCallsByConversation,

View file

@ -6,8 +6,8 @@ import { findLast } from 'lodash';
import type { WritableDB } from '../../sql/Interface';
import { markAllCallHistoryRead } from '../../sql/Server';
import { SeenStatus } from '../../MessageSeenStatus';
import { CallMode } from '../../types/Calling';
import {
CallMode,
CallDirection,
CallType,
DirectCallStatus,

View file

@ -5,15 +5,15 @@ import { assert } from 'chai';
import { v4 as generateGuid } from 'uuid';
import { jsonToObject, sql } from '../../sql/util';
import { CallMode } from '../../types/Calling';
import type { CallHistoryDetails } from '../../types/CallDisposition';
import {
CallMode,
CallDirection,
CallType,
DirectCallStatus,
GroupCallStatus,
callHistoryDetailsSchema,
} from '../../types/CallDisposition';
import type { CallHistoryDetails } from '../../types/CallDisposition';
import type {
CallHistoryDetailsFromDiskType,
MessageWithCallHistoryDetails,

View file

@ -95,12 +95,12 @@ import {
AdhocCallStatus,
DirectCallStatus,
GroupCallStatus,
CallMode,
} from '../types/CallDisposition';
import {
getBytesForPeerId,
getProtoForCallHistory,
} from '../util/callDisposition';
import { CallMode } from '../types/Calling';
import { MAX_MESSAGE_COUNT } from '../util/deleteForMe.types';
export type SendMetadataType = {

View file

@ -3,7 +3,6 @@
import { z } from 'zod';
import Long from 'long';
import { CallMode } from './Calling';
import type { AciString } from './ServiceId';
import { aciSchema } from './ServiceId';
import { bytesToUuid } from '../util/uuidToBytes';
@ -11,6 +10,13 @@ import { SignalService as Proto } from '../protobuf';
import * as Bytes from '../Bytes';
import { UUID_BYTE_SIZE } from './Crypto';
// These are strings (1) for the database (2) for Storybook.
export enum CallMode {
Direct = 'Direct',
Group = 'Group',
Adhoc = 'Adhoc',
}
export enum CallType {
Audio = 'Audio',
Video = 'Video',

View file

@ -98,3 +98,7 @@ export const callLinkRecordSchema = z.object({
expiration: z.number().int().nullable(),
revoked: z.union([z.literal(1), z.literal(0)]),
}) satisfies z.ZodType<CallLinkRecord>;
export function isCallLinkAdmin(callLink: CallLinkType): boolean {
return callLink.adminKey != null;
}

View file

@ -5,17 +5,10 @@ import type { AudioDevice, Reaction as CallReaction } from '@signalapp/ringrtc';
import type { ConversationType } from '../state/ducks/conversations';
import type { AciString, ServiceIdString } from './ServiceId';
import type { CallLinkConversationType } from './CallLink';
import type { CallMode } from './CallDisposition';
export const MAX_CALLING_REACTIONS = 5;
export const CALLING_REACTIONS_LIFETIME = 4000;
// These are strings (1) for the database (2) for Storybook.
export enum CallMode {
Direct = 'Direct',
Group = 'Group',
Adhoc = 'Adhoc',
}
// Speaker and Presentation mode have the same UI, but Presentation is only set
// automatically when someone starts to present, and will revert to the previous view mode
// once presentation is complete

View file

@ -18,11 +18,24 @@ import { DataReader, DataWriter } from '../sql/Client';
import { SignalService as Proto } from '../protobuf';
import { bytesToUuid, uuidToBytes } from './uuidToBytes';
import { missingCaseError } from './missingCaseError';
import { CallEndedReason, GroupCallJoinState } from '../types/Calling';
import {
CallEndedReason,
CallMode,
GroupCallJoinState,
} from '../types/Calling';
DirectCallStatus,
GroupCallStatus,
callEventNormalizeSchema,
CallType,
CallDirection,
callEventDetailsSchema,
LocalCallEvent,
RemoteCallEvent,
callHistoryDetailsSchema,
callDetailsSchema,
AdhocCallStatus,
CallStatusValue,
callLogEventNormalizeSchema,
CallLogEvent,
} from '../types/CallDisposition';
import type { AciString } from '../types/ServiceId';
import { isAciString } from './isAciString';
import { isMe } from './whatTypeOfConversation';
@ -49,26 +62,11 @@ import type {
CallStatus,
GroupCallMeta,
} from '../types/CallDisposition';
import {
DirectCallStatus,
GroupCallStatus,
callEventNormalizeSchema,
CallType,
CallDirection,
callEventDetailsSchema,
LocalCallEvent,
RemoteCallEvent,
callHistoryDetailsSchema,
callDetailsSchema,
AdhocCallStatus,
CallStatusValue,
callLogEventNormalizeSchema,
CallLogEvent,
} from '../types/CallDisposition';
import type { ConversationType } from '../state/ducks/conversations';
import type { ConversationModel } from '../models/conversations';
import { drop } from './drop';
import { sendCallLinkUpdateSync } from './sendCallLinkUpdateSync';
import { callLinksDeleteJobQueue } from '../jobs/callLinksDeleteJobQueue';
// utils
// -----
@ -1295,11 +1293,15 @@ export async function clearCallHistoryDataAndSync(
`clearCallHistory: Clearing call history before (${latestCall.callId}, ${latestCall.timestamp})`
);
const messageIds = await DataWriter.clearCallHistory(latestCall);
await DataWriter.beginDeleteAllCallLinks();
updateDeletedMessages(messageIds);
log.info('clearCallHistory: Queueing sync message');
await singleProtoJobQueue.add(
MessageSender.getClearCallHistoryMessage(latestCall)
);
await callLinksDeleteJobQueue.add({
source: 'clearCallHistoryDataAndSync',
});
} catch (error) {
log.error('clearCallHistory: Failed to clear call history', error);
}

View file

@ -1,46 +1,20 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { CallLinkState as RingRTCCallLinkState } from '@signalapp/ringrtc';
import {
CallLinkRootKey,
CallLinkRestrictions as RingRTCCallLinkRestrictions,
} from '@signalapp/ringrtc';
import { Aci } from '@signalapp/libsignal-client';
import { z } from 'zod';
import { v4 as generateUuid } from 'uuid';
import * as RemoteConfig from '../RemoteConfig';
import type { CallLinkAuthCredentialPresentation } from './zkgroup';
import {
CallLinkAuthCredential,
CallLinkSecretParams,
GenericServerPublicParams,
} from './zkgroup';
import { getCheckedCallLinkAuthCredentialsForToday } from '../services/groupCredentialFetcher';
import * as durations from './durations';
import * as Bytes from '../Bytes';
import type {
CallLinkConversationType,
CallLinkType,
CallLinkRecord,
CallLinkStateType,
} from '../types/CallLink';
import {
CallLinkNameMaxByteLength,
callLinkRecordSchema,
CallLinkRestrictions,
toCallLinkRestrictions,
} from '../types/CallLink';
import type { CallLinkConversationType, CallLinkType } from '../types/CallLink';
import { CallLinkRestrictions } from '../types/CallLink';
import type { LocalizerType } from '../types/Util';
import { isTestOrMockEnvironment } from '../environment';
import { getColorForCallLink } from './getColorForCallLink';
import { unicodeSlice } from './unicodeSlice';
import {
AdhocCallStatus,
CallDirection,
CallType,
type CallHistoryDetails,
CallMode,
} from '../types/CallDisposition';
import { CallMode } from '../types/Calling';
export const CALL_LINK_DEFAULT_STATE = {
name: '',
@ -56,49 +30,6 @@ export function isCallLinksCreateEnabled(): boolean {
return RemoteConfig.getValue('desktop.calling.adhoc.create') === 'TRUE';
}
export function getRoomIdFromRootKey(rootKey: CallLinkRootKey): string {
return rootKey.deriveRoomId().toString('hex');
}
export function getCallLinkRootKeyFromUrlKey(key: string): Uint8Array {
// Returns `Buffer` which inherits from `Uint8Array`
return CallLinkRootKey.parse(key).bytes;
}
export async function getCallLinkAuthCredentialPresentation(
callLinkRootKey: CallLinkRootKey
): Promise<CallLinkAuthCredentialPresentation> {
const credentials = getCheckedCallLinkAuthCredentialsForToday(
'getCallLinkAuthCredentialPresentation'
);
const todaysCredentials = credentials.today.credential;
const credential = new CallLinkAuthCredential(
Buffer.from(todaysCredentials, 'base64')
);
const genericServerPublicParamsBase64 = window.getGenericServerPublicParams();
const genericServerPublicParams = new GenericServerPublicParams(
Buffer.from(genericServerPublicParamsBase64, 'base64')
);
const ourAci = window.textsecure.storage.user.getAci();
if (ourAci == null) {
throw new Error('Failed to get our ACI');
}
const userId = Aci.fromUuid(ourAci);
const callLinkSecretParams = CallLinkSecretParams.deriveFromRootKey(
callLinkRootKey.bytes
);
const presentation = credential.present(
userId,
credentials.today.redemptionTime / durations.SECOND,
genericServerPublicParams,
callLinkSecretParams
);
return presentation;
}
export function callLinkToConversation(
callLink: CallLinkType,
i18n: LocalizerType
@ -131,14 +62,6 @@ export function getPlaceholderCallLinkConversation(
};
}
export function toRootKeyBytes(rootKey: string): Uint8Array {
return CallLinkRootKey.parse(rootKey).bytes;
}
export function fromRootKeyBytes(rootKey: Uint8Array): string {
return CallLinkRootKey.fromBytes(rootKey as Buffer).toString();
}
export function toAdminKeyBytes(adminKey: string): Buffer {
return Buffer.from(adminKey, 'base64');
}
@ -147,78 +70,6 @@ export function fromAdminKeyBytes(adminKey: Uint8Array): string {
return Bytes.toBase64(adminKey);
}
/**
* RingRTC conversions
*/
export function callLinkStateFromRingRTC(
state: RingRTCCallLinkState
): CallLinkStateType {
return {
name: unicodeSlice(state.name, 0, CallLinkNameMaxByteLength),
restrictions: toCallLinkRestrictions(state.restrictions),
revoked: state.revoked,
expiration: state.expiration.getTime(),
};
}
const RingRTCCallLinkRestrictionsSchema = z.nativeEnum(
RingRTCCallLinkRestrictions
);
export function callLinkRestrictionsToRingRTC(
restrictions: CallLinkRestrictions
): RingRTCCallLinkRestrictions {
return RingRTCCallLinkRestrictionsSchema.parse(restrictions);
}
/**
* DB record conversions
*/
export function callLinkToRecord(callLink: CallLinkType): CallLinkRecord {
if (callLink.rootKey == null) {
throw new Error('CallLink.callLinkToRecord: rootKey is null');
}
const rootKey = toRootKeyBytes(callLink.rootKey);
const adminKey = callLink.adminKey
? toAdminKeyBytes(callLink.adminKey)
: null;
return callLinkRecordSchema.parse({
roomId: callLink.roomId,
rootKey,
adminKey,
name: callLink.name,
restrictions: callLink.restrictions,
revoked: callLink.revoked ? 1 : 0,
expiration: callLink.expiration,
});
}
export function callLinkFromRecord(record: CallLinkRecord): CallLinkType {
if (record.rootKey == null) {
throw new Error('CallLink.callLinkFromRecord: rootKey is null');
}
// root keys in memory are strings for simplicity
const rootKey = fromRootKeyBytes(record.rootKey);
const adminKey = record.adminKey ? fromAdminKeyBytes(record.adminKey) : null;
return {
roomId: record.roomId,
rootKey,
adminKey,
name: record.name,
restrictions: toCallLinkRestrictions(record.restrictions),
revoked: record.revoked === 1,
expiration: record.expiration,
};
}
export function isCallLinkAdmin(callLink: CallLinkType): boolean {
return callLink.adminKey != null;
}
export function toCallHistoryFromUnusedCallLink(
callLink: CallLinkType
): CallHistoryDetails {

150
ts/util/callLinksRingrtc.ts Normal file
View file

@ -0,0 +1,150 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import {
CallLinkRestrictions as RingRTCCallLinkRestrictions,
CallLinkRootKey,
} from '@signalapp/ringrtc';
import type { CallLinkState as RingRTCCallLinkState } from '@signalapp/ringrtc';
import { z } from 'zod';
import { Aci } from '@signalapp/libsignal-client';
import type {
CallLinkRecord,
CallLinkRestrictions,
CallLinkType,
} from '../types/CallLink';
import {
type CallLinkStateType,
CallLinkNameMaxByteLength,
callLinkRecordSchema,
toCallLinkRestrictions,
} from '../types/CallLink';
import { unicodeSlice } from './unicodeSlice';
import type { CallLinkAuthCredentialPresentation } from './zkgroup';
import {
CallLinkAuthCredential,
CallLinkSecretParams,
GenericServerPublicParams,
} from './zkgroup';
import { getCheckedCallLinkAuthCredentialsForToday } from '../services/groupCredentialFetcher';
import * as durations from './durations';
import { fromAdminKeyBytes, toAdminKeyBytes } from './callLinks';
/**
* RingRTC conversions
*/
export function callLinkStateFromRingRTC(
state: RingRTCCallLinkState
): CallLinkStateType {
return {
name: unicodeSlice(state.name, 0, CallLinkNameMaxByteLength),
restrictions: toCallLinkRestrictions(state.restrictions),
revoked: state.revoked,
expiration: state.expiration.getTime(),
};
}
const RingRTCCallLinkRestrictionsSchema = z.nativeEnum(
RingRTCCallLinkRestrictions
);
export function callLinkRestrictionsToRingRTC(
restrictions: CallLinkRestrictions
): RingRTCCallLinkRestrictions {
return RingRTCCallLinkRestrictionsSchema.parse(restrictions);
}
export function getRoomIdFromRootKey(rootKey: CallLinkRootKey): string {
return rootKey.deriveRoomId().toString('hex');
}
export function getCallLinkRootKeyFromUrlKey(key: string): Uint8Array {
// Returns `Buffer` which inherits from `Uint8Array`
return CallLinkRootKey.parse(key).bytes;
}
export async function getCallLinkAuthCredentialPresentation(
callLinkRootKey: CallLinkRootKey
): Promise<CallLinkAuthCredentialPresentation> {
const credentials = getCheckedCallLinkAuthCredentialsForToday(
'getCallLinkAuthCredentialPresentation'
);
const todaysCredentials = credentials.today.credential;
const credential = new CallLinkAuthCredential(
Buffer.from(todaysCredentials, 'base64')
);
const genericServerPublicParamsBase64 = window.getGenericServerPublicParams();
const genericServerPublicParams = new GenericServerPublicParams(
Buffer.from(genericServerPublicParamsBase64, 'base64')
);
const ourAci = window.textsecure.storage.user.getAci();
if (ourAci == null) {
throw new Error('Failed to get our ACI');
}
const userId = Aci.fromUuid(ourAci);
const callLinkSecretParams = CallLinkSecretParams.deriveFromRootKey(
callLinkRootKey.bytes
);
const presentation = credential.present(
userId,
credentials.today.redemptionTime / durations.SECOND,
genericServerPublicParams,
callLinkSecretParams
);
return presentation;
}
export function toRootKeyBytes(rootKey: string): Uint8Array {
return CallLinkRootKey.parse(rootKey).bytes;
}
export function fromRootKeyBytes(rootKey: Uint8Array): string {
return CallLinkRootKey.fromBytes(rootKey as Buffer).toString();
}
/**
* DB record conversions
*/
export function callLinkFromRecord(record: CallLinkRecord): CallLinkType {
if (record.rootKey == null) {
throw new Error('CallLink.callLinkFromRecord: rootKey is null');
}
// root keys in memory are strings for simplicity
const rootKey = fromRootKeyBytes(record.rootKey);
const adminKey = record.adminKey ? fromAdminKeyBytes(record.adminKey) : null;
return {
roomId: record.roomId,
rootKey,
adminKey,
name: record.name,
restrictions: toCallLinkRestrictions(record.restrictions),
revoked: record.revoked === 1,
expiration: record.expiration,
};
}
export function callLinkToRecord(callLink: CallLinkType): CallLinkRecord {
if (callLink.rootKey == null) {
throw new Error('CallLink.callLinkToRecord: rootKey is null');
}
const rootKey = toRootKeyBytes(callLink.rootKey);
const adminKey = callLink.adminKey
? toAdminKeyBytes(callLink.adminKey)
: null;
return callLinkRecordSchema.parse({
roomId: callLink.roomId,
rootKey,
adminKey,
name: callLink.name,
restrictions: callLink.restrictions,
revoked: callLink.revoked ? 1 : 0,
expiration: callLink.expiration,
});
}

View file

@ -1,11 +1,8 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import {
CallMode,
CallState,
GroupCallConnectionState,
} from '../types/Calling';
import { CallState, GroupCallConnectionState } from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import type { ActiveCallType } from '../types/Calling';
import { isGroupOrAdhocActiveCall } from './isGroupOrAdhocCall';

View file

@ -2,16 +2,16 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { LocalizerType } from '../types/Util';
import { CallMode } from '../types/Calling';
import { missingCaseError } from './missingCaseError';
import type { CallStatus } from '../types/CallDisposition';
import {
CallMode,
CallDirection,
DirectCallStatus,
type CallHistoryDetails,
CallType,
GroupCallStatus,
} from '../types/CallDisposition';
import { missingCaseError } from './missingCaseError';
import type { CallStatus } from '../types/CallDisposition';
import type { ConversationType } from '../state/ducks/conversations';
import { strictAssert } from './assert';
import { isMoreRecentThan } from './timestamp';

View file

@ -1,7 +1,7 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { CallMode } from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
import type { ActiveCallType, ActiveGroupCallType } from '../types/Calling';
import type {
DirectCallStateType,

View file

@ -7,7 +7,7 @@ import {
peerIdToLog,
updateCallHistoryFromRemoteEvent,
} from './callDisposition';
import { CallMode } from '../types/Calling';
import { CallMode } from '../types/CallDisposition';
export async function onCallEventSync(
syncEvent: CallEventSyncEvent

View file

@ -5,9 +5,11 @@ import { CallLinkRootKey } from '@signalapp/ringrtc';
import type { CallLinkUpdateSyncEvent } from '../textsecure/messageReceiverEvents';
import * as log from '../logging/log';
import * as Errors from '../types/errors';
import { fromAdminKeyBytes, getRoomIdFromRootKey } from './callLinks';
import { fromAdminKeyBytes } from './callLinks';
import { getRoomIdFromRootKey } from './callLinksRingrtc';
import { strictAssert } from './assert';
import { CallLinkUpdateSyncType } from '../types/CallLink';
import { DataWriter } from '../sql/Client';
export async function onCallLinkUpdateSync(
syncEvent: CallLinkUpdateSyncEvent
@ -46,8 +48,9 @@ export async function onCallLinkUpdateSync(
adminKey: adminKeyString,
});
} else if (type === CallLinkUpdateSyncType.Delete) {
// TODO: DESKTOP-6951
log.warn(`${logId}: Deleting call links is not supported`);
log.info(`${logId}: Deleting call link record ${roomId}`);
await DataWriter.deleteCallLinkFromSync(roomId);
window.reduxActions.calling.handleCallLinkDelete({ roomId });
}
confirm();

View file

@ -7,7 +7,8 @@ import * as Errors from '../types/errors';
import { SignalService as Proto } from '../protobuf';
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
import MessageSender from '../textsecure/SendMessage';
import { toAdminKeyBytes, toRootKeyBytes } from './callLinks';
import { toAdminKeyBytes } from './callLinks';
import { toRootKeyBytes } from './callLinksRingrtc';
export type sendCallLinkUpdateSyncCallLinkType = {
rootKey: string;

View file

@ -386,11 +386,11 @@ export const linkCallRoute = _route('linkCall', {
},
toWebUrl(args) {
const params = new URLSearchParams({ key: args.key });
return new URL(`https://signal.link/call#${params.toString()}`);
return new URL(`https://signal.link/call/#${params.toString()}`);
},
toAppUrl(args) {
const params = new URLSearchParams({ key: args.key });
return new URL(`sgnl://signal.link/call#${params.toString()}`);
return new URL(`sgnl://signal.link/call/#${params.toString()}`);
},
});