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", "messageformat": "Share link via Signal",
"description": "Call History > Call Link Details > Share Link via Signal Button" "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": { "icu:CallLinkEditModal__Title": {
"messageformat": "Call link details", "messageformat": "Call link details",
"description": "Call Link Edit Modal > Title" "description": "Call Link Edit Modal > Title"

View file

@ -45,3 +45,17 @@
.CallLinkDetails__HeaderButton { .CallLinkDetails__HeaderButton {
font-weight: 600; 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 { getThemeType } from './util/getThemeType';
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager'; import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync'; import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
import { CallMode } from './types/Calling'; import { CallMode } from './types/CallDisposition';
import type { SyncTaskType } from './util/syncTasks'; import type { SyncTaskType } from './util/syncTasks';
import { queueSyncTasks } from './util/syncTasks'; import { queueSyncTasks } from './util/syncTasks';
import type { ViewSyncTaskType } from './messageModifiers/ViewSyncs'; 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 // Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React, { useState } from 'react';
import type { CallHistoryGroup } from '../types/CallDisposition'; import type { CallHistoryGroup } from '../types/CallDisposition';
import type { LocalizerType } from '../types/I18N'; import type { LocalizerType } from '../types/I18N';
import { CallHistoryGroupPanelSection } from './conversation/conversation-details/CallHistoryGroupPanelSection'; import { CallHistoryGroupPanelSection } from './conversation/conversation-details/CallHistoryGroupPanelSection';
@ -17,8 +17,9 @@ import { Avatar, AvatarSize } from './Avatar';
import { Button, ButtonSize, ButtonVariant } from './Button'; import { Button, ButtonSize, ButtonVariant } from './Button';
import { copyCallLink } from '../util/copyLinksWithToast'; import { copyCallLink } from '../util/copyLinksWithToast';
import { getColorForCallLink } from '../util/getColorForCallLink'; import { getColorForCallLink } from '../util/getColorForCallLink';
import { isCallLinkAdmin } from '../util/callLinks'; import { isCallLinkAdmin } from '../types/CallLink';
import { CallLinkRestrictionsSelect } from './CallLinkRestrictionsSelect'; import { CallLinkRestrictionsSelect } from './CallLinkRestrictionsSelect';
import { ConfirmationDialog } from './ConfirmationDialog';
function toUrlWithoutProtocol(url: URL): string { function toUrlWithoutProtocol(url: URL): string {
return `${url.hostname}${url.pathname}${url.search}${url.hash}`; return `${url.hostname}${url.pathname}${url.search}${url.hash}`;
@ -28,6 +29,7 @@ export type CallLinkDetailsProps = Readonly<{
callHistoryGroup: CallHistoryGroup; callHistoryGroup: CallHistoryGroup;
callLink: CallLinkType; callLink: CallLinkType;
i18n: LocalizerType; i18n: LocalizerType;
onDeleteCallLink: () => void;
onOpenCallLinkAddNameModal: () => void; onOpenCallLinkAddNameModal: () => void;
onStartCallLinkLobby: () => void; onStartCallLinkLobby: () => void;
onShareCallLinkViaSignal: () => void; onShareCallLinkViaSignal: () => void;
@ -38,11 +40,14 @@ export function CallLinkDetails({
callHistoryGroup, callHistoryGroup,
callLink, callLink,
i18n, i18n,
onDeleteCallLink,
onOpenCallLinkAddNameModal, onOpenCallLinkAddNameModal,
onStartCallLinkLobby, onStartCallLinkLobby,
onShareCallLinkViaSignal, onShareCallLinkViaSignal,
onUpdateCallLinkRestrictions, onUpdateCallLinkRestrictions,
}: CallLinkDetailsProps): JSX.Element { }: CallLinkDetailsProps): JSX.Element {
const [isDeleteCallLinkModalOpen, setIsDeleteCallLinkModalOpen] =
useState(false);
const webUrl = linkCallRoute.toWebUrl({ const webUrl = linkCallRoute.toWebUrl({
key: callLink.rootKey, key: callLink.rootKey,
}); });
@ -144,6 +149,43 @@ export function CallLinkDetails({
onClick={onShareCallLinkViaSignal} onClick={onShareCallLinkViaSignal}
/> />
</PanelSection> </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> </div>
); );
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,7 +14,8 @@ import type {
GroupCallRemoteParticipantType, GroupCallRemoteParticipantType,
GroupCallVideoRequest, GroupCallVideoRequest,
} from '../types/Calling'; } 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 { AvatarColors } from '../types/Colors';
import type { SetRendererCanvasType } from '../state/ducks/calling'; import type { SetRendererCanvasType } from '../state/ducks/calling';
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer'; import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,12 +19,7 @@ import { makeFakeLookupConversationWithoutServiceId } from '../../../test-both/h
import { ThemeType } from '../../../types/Util'; import { ThemeType } from '../../../types/Util';
import { DurationInSeconds } from '../../../util/durations'; import { DurationInSeconds } from '../../../util/durations';
import { NavTab } from '../../../state/ducks/nav'; import { NavTab } from '../../../state/ducks/nav';
import { CallMode } from '../../../types/Calling'; import { getFakeCallHistoryGroup } from '../../../test-both/helpers/getFakeCallHistoryGroup';
import {
CallDirection,
CallType,
DirectCallStatus,
} from '../../../types/CallDisposition';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -224,30 +219,15 @@ export const _11 = (): JSX.Element => (
<ConversationDetails {...createProps()} isGroup={false} /> <ConversationDetails {...createProps()} isGroup={false} />
); );
function mins(n: number) {
return DurationInSeconds.toMillis(DurationInSeconds.fromMinutes(n));
}
export function WithCallHistoryGroup(): JSX.Element { export function WithCallHistoryGroup(): JSX.Element {
const props = createProps(); const props = createProps();
return ( return (
<ConversationDetails <ConversationDetails
{...props} {...props}
callHistoryGroup={{ callHistoryGroup={getFakeCallHistoryGroup({
peerId: props.conversation?.serviceId ?? '', 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) },
],
}}
selectedNavTab={NavTab.Calls} 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 type { WebAPIType } from '../textsecure/WebAPI';
import { drop } from '../util/drop'; import { drop } from '../util/drop';
import { callLinksDeleteJobQueue } from './callLinksDeleteJobQueue';
import { conversationJobQueue } from './conversationJobQueue'; import { conversationJobQueue } from './conversationJobQueue';
import { groupAvatarJobQueue } from './groupAvatarJobQueue'; import { groupAvatarJobQueue } from './groupAvatarJobQueue';
@ -40,6 +41,7 @@ export function initializeAllJobQueues({
// Other queues // Other queues
drop(removeStorageKeyJobQueue.streamJobs()); drop(removeStorageKeyJobQueue.streamJobs());
drop(reportSpamJobQueue.streamJobs()); drop(reportSpamJobQueue.streamJobs());
drop(callLinksDeleteJobQueue.streamJobs());
} }
export async function shutdownAllJobQueues(): Promise<void> { export async function shutdownAllJobQueues(): Promise<void> {
@ -52,5 +54,6 @@ export async function shutdownAllJobQueues(): Promise<void> {
viewOnceOpenJobQueue.shutdown(), viewOnceOpenJobQueue.shutdown(),
removeStorageKeyJobQueue.shutdown(), removeStorageKeyJobQueue.shutdown(),
reportSpamJobQueue.shutdown(), reportSpamJobQueue.shutdown(),
callLinksDeleteJobQueue.shutdown(),
]); ]);
} }

View file

@ -68,11 +68,11 @@ import type {
PresentedSource, PresentedSource,
} from '../types/Calling'; } from '../types/Calling';
import { import {
CallMode,
GroupCallConnectionState, GroupCallConnectionState,
GroupCallJoinState, GroupCallJoinState,
ScreenShareStatus, ScreenShareStatus,
} from '../types/Calling'; } from '../types/Calling';
import { CallMode, LocalCallEvent } from '../types/CallDisposition';
import { import {
findBestMatchingAudioDeviceIndex, findBestMatchingAudioDeviceIndex,
findBestMatchingCameraId, findBestMatchingCameraId,
@ -135,17 +135,16 @@ import {
getCallDetailsForAdhocCall, getCallDetailsForAdhocCall,
} from '../util/callDisposition'; } from '../util/callDisposition';
import { isNormalNumber } from '../util/isNormalNumber'; import { isNormalNumber } from '../util/isNormalNumber';
import { LocalCallEvent } from '../types/CallDisposition';
import type { AciString, ServiceIdString } from '../types/ServiceId'; import type { AciString, ServiceIdString } from '../types/ServiceId';
import { isServiceIdString } from '../types/ServiceId'; import { isServiceIdString } from '../types/ServiceId';
import { isInSystemContacts } from '../util/isInSystemContacts'; import { isInSystemContacts } from '../util/isInSystemContacts';
import { toAdminKeyBytes } from '../util/callLinks';
import { import {
getRoomIdFromRootKey,
getCallLinkAuthCredentialPresentation, getCallLinkAuthCredentialPresentation,
toAdminKeyBytes, getRoomIdFromRootKey,
callLinkRestrictionsToRingRTC, callLinkRestrictionsToRingRTC,
callLinkStateFromRingRTC, callLinkStateFromRingRTC,
} from '../util/callLinks'; } from '../util/callLinksRingrtc';
import { isAdhocCallingEnabled } from '../util/isAdhocCallingEnabled'; import { isAdhocCallingEnabled } from '../util/isAdhocCallingEnabled';
import { import {
conversationJobQueue, conversationJobQueue,
@ -154,7 +153,10 @@ import {
import type { CallLinkType, CallLinkStateType } from '../types/CallLink'; import type { CallLinkType, CallLinkStateType } from '../types/CallLink';
import { CallLinkRestrictions } from '../types/CallLink'; import { CallLinkRestrictions } from '../types/CallLink';
import { getConversationIdForLogging } from '../util/idForLogging'; import { getConversationIdForLogging } from '../util/idForLogging';
import { sendCallLinkUpdateSync } from '../util/sendCallLinkUpdateSync'; import {
sendCallLinkDeleteSync,
sendCallLinkUpdateSync,
} from '../util/sendCallLinkUpdateSync';
import { createIdenticon } from '../util/createIdenticon'; import { createIdenticon } from '../util/createIdenticon';
import { getColorForCallLink } from '../util/getColorForCallLink'; import { getColorForCallLink } from '../util/getColorForCallLink';
@ -683,6 +685,41 @@ export class CallingClass {
return callLink; 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( async updateCallLinkName(
callLink: CallLinkType, callLink: CallLinkType,
name: string name: string

View file

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

View file

@ -168,6 +168,7 @@ import {
GroupCallStatus, GroupCallStatus,
CallType, CallType,
CallStatusValue, CallStatusValue,
CallMode,
} from '../types/CallDisposition'; } from '../types/CallDisposition';
import { import {
callLinkExists, callLinkExists,
@ -176,13 +177,17 @@ import {
insertCallLink, insertCallLink,
updateCallLinkAdminKeyByRoomId, updateCallLinkAdminKeyByRoomId,
updateCallLinkState, updateCallLinkState,
beginDeleteAllCallLinks,
getAllMarkedDeletedCallLinks,
finalizeDeleteCallLink,
beginDeleteCallLink,
deleteCallLinkFromSync,
} from './server/callLinks'; } from './server/callLinks';
import { import {
replaceAllEndorsementsForGroup, replaceAllEndorsementsForGroup,
deleteAllEndorsementsForGroup, deleteAllEndorsementsForGroup,
getGroupSendCombinedEndorsementExpiration, getGroupSendCombinedEndorsementExpiration,
} from './server/groupEndorsements'; } from './server/groupEndorsements';
import { CallMode } from '../types/Calling';
import { import {
attachmentDownloadJobSchema, attachmentDownloadJobSchema,
type AttachmentDownloadJobType, type AttachmentDownloadJobType,
@ -296,6 +301,7 @@ export const DataReader: ServerReadableInterface = {
callLinkExists, callLinkExists,
getAllCallLinks, getAllCallLinks,
getCallLinkByRoomId, getCallLinkByRoomId,
getAllMarkedDeletedCallLinks,
getMessagesBetween, getMessagesBetween,
getNearbyMessageFromDeletedSet, getNearbyMessageFromDeletedSet,
getMostRecentAddressableMessages, getMostRecentAddressableMessages,
@ -430,6 +436,10 @@ export const DataWriter: ServerWritableInterface = {
insertCallLink, insertCallLink,
updateCallLinkAdminKeyByRoomId, updateCallLinkAdminKeyByRoomId,
updateCallLinkState, updateCallLinkState,
beginDeleteAllCallLinks,
beginDeleteCallLink,
finalizeDeleteCallLink,
deleteCallLinkFromSync,
migrateConversationMessages, migrateConversationMessages,
saveEditedMessage, saveEditedMessage,
saveEditedMessages, saveEditedMessages,
@ -3458,7 +3468,7 @@ function clearCallHistory(
return db.transaction(() => { return db.transaction(() => {
const timestamp = getMessageTimestampForCallLogEventTarget(db, target); const timestamp = getMessageTimestampForCallLogEventTarget(db, target);
const [selectCallIdsQuery, selectCallIdsParams] = sql` const [selectCallsQuery, selectCallsParams] = sql`
SELECT callsHistory.callId SELECT callsHistory.callId
FROM callsHistory FROM callsHistory
WHERE WHERE
@ -3471,18 +3481,30 @@ function clearCallHistory(
); );
`; `;
const callIds = db const deletedCallIds: ReadonlyArray<string> = db
.prepare(selectCallIdsQuery) .prepare(selectCallsQuery)
.pluck() .pluck()
.all(selectCallIdsParams); .all(selectCallsParams);
let deletedMessageIds: ReadonlyArray<string> = []; 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` const [deleteMessagesQuery, deleteMessagesParams] = sql`
DELETE FROM messages DELETE FROM messages
WHERE messages.type IS 'call-history' WHERE messages.type IS 'call-history'
AND messages.callId IN (${sqlJoin(ids)}) AND messages.callId IN (${idsFragment})
RETURNING id; RETURNING id;
`; `;
@ -3494,21 +3516,6 @@ function clearCallHistory(
deletedMessageIds = deletedMessageIds.concat(batchDeletedMessageIds); 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; return deletedMessageIds;
})(); })();
} }
@ -3519,9 +3526,8 @@ function markCallHistoryDeleted(db: WritableDB, callId: string): void {
SET SET
status = ${DirectCallStatus.Deleted}, status = ${DirectCallStatus.Deleted},
timestamp = ${Date.now()} timestamp = ${Date.now()}
WHERE callId = ${callId}; WHERE callId = ${callId}
`; `;
db.prepare(query).run(params); db.prepare(query).run(params);
} }
@ -4829,7 +4835,7 @@ function getNextAttachmentBackupJobs(
active = 0 active = 0
AND AND
(retryAfter is NULL OR retryAfter <= ${timestamp}) (retryAfter is NULL OR retryAfter <= ${timestamp})
ORDER BY ORDER BY
-- type is "standard" or "thumbnail"; we prefer "standard" jobs -- type is "standard" or "thumbnail"; we prefer "standard" jobs
type ASC, receivedAt DESC type ASC, receivedAt DESC
LIMIT ${limit} 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, CallType,
GroupCallStatus, GroupCallStatus,
callHistoryDetailsSchema, callHistoryDetailsSchema,
CallMode,
} from '../../types/CallDisposition'; } from '../../types/CallDisposition';
import { CallMode } from '../../types/Calling';
import type { WritableDB, MessageType, ConversationType } from '../Interface'; import type { WritableDB, MessageType, ConversationType } from '../Interface';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { missingCaseError } from '../../util/missingCaseError'; 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 { updateToSchemaVersion1100 } from './1100-optimize-mark-call-history-read-in-conversation';
import { updateToSchemaVersion1110 } from './1110-sticker-local-key'; import { updateToSchemaVersion1110 } from './1110-sticker-local-key';
import { updateToSchemaVersion1120 } from './1120-messages-foreign-keys-indexes'; import { updateToSchemaVersion1120 } from './1120-messages-foreign-keys-indexes';
import { updateToSchemaVersion1130 } from './1130-isStory-index';
import { import {
updateToSchemaVersion1130, updateToSchemaVersion1140,
version as MAX_VERSION, version as MAX_VERSION,
} from './1130-isStory-index'; } from './1140-call-links-deleted-column';
function updateToSchemaVersion1( function updateToSchemaVersion1(
currentVersion: number, currentVersion: number,
@ -2050,6 +2051,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion1110, updateToSchemaVersion1110,
updateToSchemaVersion1120, updateToSchemaVersion1120,
updateToSchemaVersion1130, updateToSchemaVersion1130,
updateToSchemaVersion1140,
]; ];
export class DBVersionFromFutureError extends Error { export class DBVersionFromFutureError extends Error {

View file

@ -7,15 +7,16 @@ import {
callLinkRestrictionsSchema, callLinkRestrictionsSchema,
callLinkRecordSchema, callLinkRecordSchema,
} from '../../types/CallLink'; } from '../../types/CallLink';
import { toAdminKeyBytes } from '../../util/callLinks';
import { import {
callLinkToRecord, callLinkToRecord,
callLinkFromRecord, callLinkFromRecord,
toAdminKeyBytes, } from '../../util/callLinksRingrtc';
} from '../../util/callLinks';
import type { ReadableDB, WritableDB } from '../Interface'; import type { ReadableDB, WritableDB } from '../Interface';
import { prepare } from '../Server'; import { prepare } from '../Server';
import { sql } from '../util'; import { sql } from '../util';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { CallStatusValue } from '../../types/CallDisposition';
export function callLinkExists(db: ReadableDB, roomId: string): boolean { export function callLinkExists(db: ReadableDB, roomId: string): boolean {
const [query, params] = sql` const [query, params] = sql`
@ -133,3 +134,106 @@ function assertRoomIdMatchesRootKey(roomId: string, rootKey: string): void {
'passed roomId must match roomId derived from root key' '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, getCallHistoryLatestCall,
getCallHistorySelector, getCallHistorySelector,
} from '../selectors/callHistory'; } from '../selectors/callHistory';
import {
getCallsHistoryForRedux,
getCallsHistoryUnreadCountForRedux,
loadCallsHistory,
} from '../../services/callHistoryLoader';
import { makeLookup } from '../../util/makeLookup';
export type CallHistoryState = ReadonlyDeep<{ export type CallHistoryState = ReadonlyDeep<{
// This informs the app that underlying call history data has changed. // 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_ADD = 'callHistory/ADD';
const CALL_HISTORY_REMOVE = 'callHistory/REMOVE'; const CALL_HISTORY_REMOVE = 'callHistory/REMOVE';
const CALL_HISTORY_RESET = 'callHistory/RESET'; const CALL_HISTORY_RESET = 'callHistory/RESET';
const CALL_HISTORY_RELOAD = 'callHistory/RELOAD';
const CALL_HISTORY_UPDATE_UNREAD = 'callHistory/UPDATE_UNREAD'; const CALL_HISTORY_UPDATE_UNREAD = 'callHistory/UPDATE_UNREAD';
export type CallHistoryAdd = ReadonlyDeep<{ export type CallHistoryAdd = ReadonlyDeep<{
@ -50,6 +57,14 @@ export type CallHistoryReset = ReadonlyDeep<{
type: typeof CALL_HISTORY_RESET; type: typeof CALL_HISTORY_RESET;
}>; }>;
export type CallHistoryReload = ReadonlyDeep<{
type: typeof CALL_HISTORY_RELOAD;
payload: {
callsHistory: ReadonlyArray<CallHistoryDetails>;
callsHistoryUnreadCount: number;
};
}>;
export type CallHistoryUpdateUnread = ReadonlyDeep<{ export type CallHistoryUpdateUnread = ReadonlyDeep<{
type: typeof CALL_HISTORY_UPDATE_UNREAD; type: typeof CALL_HISTORY_UPDATE_UNREAD;
payload: number; payload: number;
@ -59,6 +74,7 @@ export type CallHistoryAction = ReadonlyDeep<
| CallHistoryAdd | CallHistoryAdd
| CallHistoryRemove | CallHistoryRemove
| CallHistoryReset | CallHistoryReset
| CallHistoryReload
| CallHistoryUpdateUnread | CallHistoryUpdateUnread
>; >;
@ -178,9 +194,29 @@ function clearAllCallHistory(): ThunkAction<
} catch (error) { } catch (error) {
log.error('Error clearing call history', Errors.toLogFormat(error)); log.error('Error clearing call history', Errors.toLogFormat(error));
} finally { } finally {
// Just force a reset, even if the clear failed. // Just force a reload, even if the clear failed.
dispatch(resetCallHistory()); dispatch(reloadCallHistory());
dispatch(updateCallHistoryUnreadCount()); }
};
}
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, ...state,
unreadCount: action.payload, unreadCount: action.payload,
}; };
case CALL_HISTORY_RELOAD:
return {
edition: state.edition + 1,
unreadCount: action.payload.callsHistoryUnreadCount,
callHistoryByCallId: makeLookup(action.payload.callsHistory, 'callId'),
};
default: default:
return state; return state;
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,7 +32,8 @@ import type {
ConversationsByDemuxIdType, ConversationsByDemuxIdType,
GroupCallRemoteParticipantType, GroupCallRemoteParticipantType,
} from '../../types/Calling'; } 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 type { AciString } from '../../types/ServiceId';
import { strictAssert } from '../../util/assert'; import { strictAssert } from '../../util/assert';
import { callLinkToConversation } from '../../util/callLinks'; import { callLinkToConversation } from '../../util/callLinks';

View file

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

View file

@ -10,7 +10,7 @@ import {
} from '../../components/conversation/ConversationHeader'; } from '../../components/conversation/ConversationHeader';
import { getCannotLeaveBecauseYouAreLastAdmin } from '../../components/conversation/conversation-details/ConversationDetails'; import { getCannotLeaveBecauseYouAreLastAdmin } from '../../components/conversation/conversation-details/ConversationDetails';
import { useMinimalConversation } from '../../hooks/useMinimalConversation'; import { useMinimalConversation } from '../../hooks/useMinimalConversation';
import { CallMode } from '../../types/Calling'; import { CallMode } from '../../types/CallDisposition';
import { PanelType } from '../../types/Panels'; import { PanelType } from '../../types/Panels';
import { StoryViewModeType } from '../../types/Stories'; import { StoryViewModeType } from '../../types/Stories';
import { strictAssert } from '../../util/assert'; 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 { assert } from 'chai';
import { callLinkToRecord, callLinkFromRecord } from '../../util/callLinks'; import {
callLinkToRecord,
callLinkFromRecord,
} from '../../util/callLinksRingrtc';
import { import {
FAKE_CALL_LINK as CALL_LINK, FAKE_CALL_LINK as CALL_LINK,
FAKE_CALL_LINK_WITH_ADMIN_KEY as CALL_LINK_WITH_ADMIN_KEY, FAKE_CALL_LINK_WITH_ADMIN_KEY as CALL_LINK_WITH_ADMIN_KEY,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,6 @@
import { z } from 'zod'; import { z } from 'zod';
import Long from 'long'; import Long from 'long';
import { CallMode } from './Calling';
import type { AciString } from './ServiceId'; import type { AciString } from './ServiceId';
import { aciSchema } from './ServiceId'; import { aciSchema } from './ServiceId';
import { bytesToUuid } from '../util/uuidToBytes'; import { bytesToUuid } from '../util/uuidToBytes';
@ -11,6 +10,13 @@ import { SignalService as Proto } from '../protobuf';
import * as Bytes from '../Bytes'; import * as Bytes from '../Bytes';
import { UUID_BYTE_SIZE } from './Crypto'; 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 { export enum CallType {
Audio = 'Audio', Audio = 'Audio',
Video = 'Video', Video = 'Video',

View file

@ -98,3 +98,7 @@ export const callLinkRecordSchema = z.object({
expiration: z.number().int().nullable(), expiration: z.number().int().nullable(),
revoked: z.union([z.literal(1), z.literal(0)]), revoked: z.union([z.literal(1), z.literal(0)]),
}) satisfies z.ZodType<CallLinkRecord>; }) 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 { ConversationType } from '../state/ducks/conversations';
import type { AciString, ServiceIdString } from './ServiceId'; import type { AciString, ServiceIdString } from './ServiceId';
import type { CallLinkConversationType } from './CallLink'; import type { CallLinkConversationType } from './CallLink';
import type { CallMode } from './CallDisposition';
export const MAX_CALLING_REACTIONS = 5; export const MAX_CALLING_REACTIONS = 5;
export const CALLING_REACTIONS_LIFETIME = 4000; 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 // 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 // automatically when someone starts to present, and will revert to the previous view mode
// once presentation is complete // once presentation is complete

View file

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

View file

@ -1,46 +1,20 @@
// Copyright 2024 Signal Messenger, LLC // Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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 { v4 as generateUuid } from 'uuid';
import * as RemoteConfig from '../RemoteConfig'; 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 * as Bytes from '../Bytes';
import type { import type { CallLinkConversationType, CallLinkType } from '../types/CallLink';
CallLinkConversationType, import { CallLinkRestrictions } from '../types/CallLink';
CallLinkType,
CallLinkRecord,
CallLinkStateType,
} from '../types/CallLink';
import {
CallLinkNameMaxByteLength,
callLinkRecordSchema,
CallLinkRestrictions,
toCallLinkRestrictions,
} from '../types/CallLink';
import type { LocalizerType } from '../types/Util'; import type { LocalizerType } from '../types/Util';
import { isTestOrMockEnvironment } from '../environment'; import { isTestOrMockEnvironment } from '../environment';
import { getColorForCallLink } from './getColorForCallLink'; import { getColorForCallLink } from './getColorForCallLink';
import { unicodeSlice } from './unicodeSlice';
import { import {
AdhocCallStatus, AdhocCallStatus,
CallDirection, CallDirection,
CallType, CallType,
type CallHistoryDetails, type CallHistoryDetails,
CallMode,
} from '../types/CallDisposition'; } from '../types/CallDisposition';
import { CallMode } from '../types/Calling';
export const CALL_LINK_DEFAULT_STATE = { export const CALL_LINK_DEFAULT_STATE = {
name: '', name: '',
@ -56,49 +30,6 @@ export function isCallLinksCreateEnabled(): boolean {
return RemoteConfig.getValue('desktop.calling.adhoc.create') === 'TRUE'; 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( export function callLinkToConversation(
callLink: CallLinkType, callLink: CallLinkType,
i18n: LocalizerType 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 { export function toAdminKeyBytes(adminKey: string): Buffer {
return Buffer.from(adminKey, 'base64'); return Buffer.from(adminKey, 'base64');
} }
@ -147,78 +70,6 @@ export function fromAdminKeyBytes(adminKey: Uint8Array): string {
return Bytes.toBase64(adminKey); 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( export function toCallHistoryFromUnusedCallLink(
callLink: CallLinkType callLink: CallLinkType
): CallHistoryDetails { ): 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 // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { import { CallState, GroupCallConnectionState } from '../types/Calling';
CallMode, import { CallMode } from '../types/CallDisposition';
CallState,
GroupCallConnectionState,
} from '../types/Calling';
import type { ActiveCallType } from '../types/Calling'; import type { ActiveCallType } from '../types/Calling';
import { isGroupOrAdhocActiveCall } from './isGroupOrAdhocCall'; import { isGroupOrAdhocActiveCall } from './isGroupOrAdhocCall';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -386,11 +386,11 @@ export const linkCallRoute = _route('linkCall', {
}, },
toWebUrl(args) { toWebUrl(args) {
const params = new URLSearchParams({ key: args.key }); 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) { toAppUrl(args) {
const params = new URLSearchParams({ key: args.key }); 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()}`);
}, },
}); });