Support delete for call links
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
parent
11fed7e7f8
commit
9a9f9495f1
67 changed files with 853 additions and 345 deletions
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
40
ts/components/CallLinkDetails.stories.tsx
Normal file
40
ts/components/CallLinkDetails.stories.tsx
Normal 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} />;
|
||||||
|
}
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
70
ts/jobs/callLinksDeleteJobQueue.ts
Normal file
70
ts/jobs/callLinksDeleteJobQueue.ts
Normal 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,
|
||||||
|
});
|
|
@ -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(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
31
ts/sql/migrations/1140-call-links-deleted-column.ts
Normal file
31
ts/sql/migrations/1140-call-links-deleted-column.ts
Normal 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!');
|
||||||
|
}
|
|
@ -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';
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
47
ts/test-both/helpers/getFakeCallHistoryGroup.ts
Normal file
47
ts/test-both/helpers/getFakeCallHistoryGroup.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
150
ts/util/callLinksRingrtc.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue