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",
|
||||
"description": "Call History > Call Link Details > Share Link via Signal Button"
|
||||
},
|
||||
"icu:CallLinkDetails__DeleteLink": {
|
||||
"messageformat": "Delete link",
|
||||
"description": "Call History > Call Link Details > Delete Link Button"
|
||||
},
|
||||
"icu:CallLinkDetails__DeleteLinkModal__Title": {
|
||||
"messageformat": "Delete call link?",
|
||||
"description": "Call History > Call Link Details > Delete Link Modal > Title"
|
||||
},
|
||||
"icu:CallLinkDetails__DeleteLinkModal__Body": {
|
||||
"messageformat": "This link will no longer work for anyone who has it.",
|
||||
"description": "Call History > Call Link Details > Delete Link Modal > Body"
|
||||
},
|
||||
"icu:CallLinkDetails__DeleteLinkModal__Cancel": {
|
||||
"messageformat": "Cancel",
|
||||
"description": "Call History > Call Link Details > Delete Link Modal > Cancel Button"
|
||||
},
|
||||
"icu:CallLinkDetails__DeleteLinkModal__Delete": {
|
||||
"messageformat": "Delete",
|
||||
"description": "Call History > Call Link Details > Delete Link Modal > Delete Button"
|
||||
},
|
||||
"icu:CallLinkEditModal__Title": {
|
||||
"messageformat": "Call link details",
|
||||
"description": "Call Link Edit Modal > Title"
|
||||
|
|
|
@ -45,3 +45,17 @@
|
|||
.CallLinkDetails__HeaderButton {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.CallLinkDetails__DeleteLink {
|
||||
// Override the default icon color
|
||||
.ConversationDetails-icon__icon--trash::after {
|
||||
@include any-theme {
|
||||
background-color: $color-accent-red;
|
||||
}
|
||||
}
|
||||
|
||||
// Override the default label color
|
||||
.ConversationDetails-panel-row__label {
|
||||
color: $color-accent-red;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -199,7 +199,7 @@ import { deriveStorageServiceKey } from './Crypto';
|
|||
import { getThemeType } from './util/getThemeType';
|
||||
import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager';
|
||||
import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync';
|
||||
import { CallMode } from './types/Calling';
|
||||
import { CallMode } from './types/CallDisposition';
|
||||
import type { SyncTaskType } from './util/syncTasks';
|
||||
import { queueSyncTasks } from './util/syncTasks';
|
||||
import type { ViewSyncTaskType } from './messageModifiers/ViewSyncs';
|
||||
|
|
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
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import type { CallHistoryGroup } from '../types/CallDisposition';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import { CallHistoryGroupPanelSection } from './conversation/conversation-details/CallHistoryGroupPanelSection';
|
||||
|
@ -17,8 +17,9 @@ import { Avatar, AvatarSize } from './Avatar';
|
|||
import { Button, ButtonSize, ButtonVariant } from './Button';
|
||||
import { copyCallLink } from '../util/copyLinksWithToast';
|
||||
import { getColorForCallLink } from '../util/getColorForCallLink';
|
||||
import { isCallLinkAdmin } from '../util/callLinks';
|
||||
import { isCallLinkAdmin } from '../types/CallLink';
|
||||
import { CallLinkRestrictionsSelect } from './CallLinkRestrictionsSelect';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
|
||||
function toUrlWithoutProtocol(url: URL): string {
|
||||
return `${url.hostname}${url.pathname}${url.search}${url.hash}`;
|
||||
|
@ -28,6 +29,7 @@ export type CallLinkDetailsProps = Readonly<{
|
|||
callHistoryGroup: CallHistoryGroup;
|
||||
callLink: CallLinkType;
|
||||
i18n: LocalizerType;
|
||||
onDeleteCallLink: () => void;
|
||||
onOpenCallLinkAddNameModal: () => void;
|
||||
onStartCallLinkLobby: () => void;
|
||||
onShareCallLinkViaSignal: () => void;
|
||||
|
@ -38,11 +40,14 @@ export function CallLinkDetails({
|
|||
callHistoryGroup,
|
||||
callLink,
|
||||
i18n,
|
||||
onDeleteCallLink,
|
||||
onOpenCallLinkAddNameModal,
|
||||
onStartCallLinkLobby,
|
||||
onShareCallLinkViaSignal,
|
||||
onUpdateCallLinkRestrictions,
|
||||
}: CallLinkDetailsProps): JSX.Element {
|
||||
const [isDeleteCallLinkModalOpen, setIsDeleteCallLinkModalOpen] =
|
||||
useState(false);
|
||||
const webUrl = linkCallRoute.toWebUrl({
|
||||
key: callLink.rootKey,
|
||||
});
|
||||
|
@ -144,6 +149,43 @@ export function CallLinkDetails({
|
|||
onClick={onShareCallLinkViaSignal}
|
||||
/>
|
||||
</PanelSection>
|
||||
{isCallLinkAdmin(callLink) && (
|
||||
<PanelSection>
|
||||
<PanelRow
|
||||
className="CallLinkDetails__DeleteLink"
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('icu:CallLinkDetails__DeleteLink')}
|
||||
icon={IconType.trash}
|
||||
/>
|
||||
}
|
||||
label={i18n('icu:CallLinkDetails__DeleteLink')}
|
||||
onClick={() => {
|
||||
setIsDeleteCallLinkModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
</PanelSection>
|
||||
)}
|
||||
{isDeleteCallLinkModalOpen && (
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
dialogName="CallLinkDetails__DeleteLinkModal"
|
||||
title={i18n('icu:CallLinkDetails__DeleteLinkModal__Title')}
|
||||
cancelText={i18n('icu:CallLinkDetails__DeleteLinkModal__Cancel')}
|
||||
actions={[
|
||||
{
|
||||
text: i18n('icu:CallLinkDetails__DeleteLinkModal__Delete'),
|
||||
style: 'affirmative',
|
||||
action: onDeleteCallLink,
|
||||
},
|
||||
]}
|
||||
onClose={() => {
|
||||
setIsDeleteCallLinkModalOpen(false);
|
||||
}}
|
||||
>
|
||||
{i18n('icu:CallLinkDetails__DeleteLinkModal__Body')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,12 +8,12 @@ import type { PropsType } from './CallManager';
|
|||
import { CallManager } from './CallManager';
|
||||
import {
|
||||
CallEndedReason,
|
||||
CallMode,
|
||||
CallState,
|
||||
CallViewMode,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
} from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import type {
|
||||
ActiveGroupCallType,
|
||||
GroupCallRemoteParticipantType,
|
||||
|
|
|
@ -20,11 +20,11 @@ import type {
|
|||
} from '../types/Calling';
|
||||
import {
|
||||
CallEndedReason,
|
||||
CallMode,
|
||||
CallState,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
} from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type {
|
||||
AcceptCallType,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
|
||||
export type PropsType = {
|
||||
callMode: CallMode;
|
||||
|
|
|
@ -12,12 +12,12 @@ import type {
|
|||
GroupCallRemoteParticipantType,
|
||||
} from '../types/Calling';
|
||||
import {
|
||||
CallMode,
|
||||
CallViewMode,
|
||||
CallState,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
} from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import { generateAci } from '../types/ServiceId';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
|
|
|
@ -33,12 +33,12 @@ import type {
|
|||
} from '../types/Calling';
|
||||
import {
|
||||
CALLING_REACTIONS_LIFETIME,
|
||||
CallMode,
|
||||
CallViewMode,
|
||||
CallState,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
} from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import type { ServiceIdString } from '../types/ServiceId';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
getDefaultConversationWithServiceId,
|
||||
} from '../test-both/helpers/getDefaultConversation';
|
||||
import { CallingToastProvider } from './CallingToast';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import { getDefaultCallLinkConversation } from '../test-both/helpers/fakeCallLink';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
CallingLobbyJoinButton,
|
||||
CallingLobbyJoinButtonVariant,
|
||||
} from './CallingLobbyJoinButton';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import type { CallingConversationType } from '../types/Calling';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { useIsOnline } from '../hooks/useIsOnline';
|
||||
|
|
|
@ -11,12 +11,12 @@ import type { PropsType } from './CallingPip';
|
|||
import { CallingPip } from './CallingPip';
|
||||
import type { ActiveDirectCallType } from '../types/Calling';
|
||||
import {
|
||||
CallMode,
|
||||
CallViewMode,
|
||||
CallState,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
} from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { fakeGetGroupCallVideoFrameSource } from '../test-both/helpers/fakeGetGroupCallVideoFrameSource';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
|
|
|
@ -14,7 +14,8 @@ import type {
|
|||
GroupCallRemoteParticipantType,
|
||||
GroupCallVideoRequest,
|
||||
} from '../types/Calling';
|
||||
import { CallMode, GroupCallJoinState } from '../types/Calling';
|
||||
import { GroupCallJoinState } from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
import type { SetRendererCanvasType } from '../state/ducks/calling';
|
||||
import { useGetCallingFrameBuffer } from '../calling/useGetCallingFrameBuffer';
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import type { ActiveCallType } from '../types/Calling';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { CallingToastProvider, useCallingToasts } from './CallingToast';
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
isSameCallHistoryGroup,
|
||||
CallMode,
|
||||
} from '../types/CallDisposition';
|
||||
import { formatDateTimeShort, isMoreRecentThan } from '../util/timestamp';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
|
@ -47,7 +48,6 @@ import { CallsNewCallButton } from './CallsNewCall';
|
|||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||
import { Theme } from '../util/theme';
|
||||
import type { CallingConversationType } from '../types/Calling';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import type { CallLinkType } from '../types/CallLink';
|
||||
import {
|
||||
callLinkToConversation,
|
||||
|
|
|
@ -62,7 +62,8 @@ type CallsTabProps = Readonly<{
|
|||
preferredLeftPaneWidth: number;
|
||||
renderCallLinkDetails: (
|
||||
roomId: string,
|
||||
callHistoryGroup: CallHistoryGroup
|
||||
callHistoryGroup: CallHistoryGroup,
|
||||
onClose: () => void
|
||||
) => JSX.Element;
|
||||
renderConversationDetails: (
|
||||
conversationId: string,
|
||||
|
@ -152,6 +153,10 @@ export function CallsTab({
|
|||
[updateSelectedView]
|
||||
);
|
||||
|
||||
const onCloseSelectedView = useCallback(() => {
|
||||
updateSelectedView(null);
|
||||
}, [updateSelectedView]);
|
||||
|
||||
useEscapeHandling(
|
||||
sidebarView === CallsTabSidebarView.NewCallView
|
||||
? () => {
|
||||
|
@ -328,7 +333,8 @@ export function CallsTab({
|
|||
{selectedView.type === 'callLink' &&
|
||||
renderCallLinkDetails(
|
||||
selectedView.roomId,
|
||||
selectedView.callHistoryGroup
|
||||
selectedView.callHistoryGroup,
|
||||
onCloseSelectedView
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { action } from '@storybook/addon-actions';
|
|||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './IncomingCallBar';
|
||||
import { IncomingCallBar } from './IncomingCallBar';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
|
|
|
@ -11,7 +11,7 @@ import { getParticipantName } from '../util/callingGetParticipantName';
|
|||
import { ContactName } from './conversation/ContactName';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { AvatarColors } from '../types/Colors';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { AcceptCallType, DeclineCallType } from '../state/ducks/calling';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
|
|
@ -6,7 +6,13 @@ import { action } from '@storybook/addon-actions';
|
|||
import type { Meta } from '@storybook/react';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import {
|
||||
CallMode,
|
||||
CallType,
|
||||
CallDirection,
|
||||
GroupCallStatus,
|
||||
DirectCallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import { generateAci } from '../../types/ServiceId';
|
||||
import { CallingNotification, type PropsType } from './CallingNotification';
|
||||
import {
|
||||
|
@ -14,12 +20,6 @@ import {
|
|||
getDefaultGroup,
|
||||
} from '../../test-both/helpers/getDefaultConversation';
|
||||
import type { CallStatus } from '../../types/CallDisposition';
|
||||
import {
|
||||
CallType,
|
||||
CallDirection,
|
||||
GroupCallStatus,
|
||||
DirectCallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
|
|
@ -10,7 +10,13 @@ import { SystemMessage, SystemMessageKind } from './SystemMessage';
|
|||
import { Button, ButtonSize, ButtonVariant } from '../Button';
|
||||
import { MessageTimestamp } from './MessageTimestamp';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import {
|
||||
CallMode,
|
||||
CallDirection,
|
||||
CallType,
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import type { CallingNotificationType } from '../../util/callingNotification';
|
||||
import {
|
||||
getCallingIcon,
|
||||
|
@ -19,12 +25,6 @@ import {
|
|||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { Tooltip, TooltipPlacement } from '../Tooltip';
|
||||
import * as log from '../../logging/log';
|
||||
import {
|
||||
CallDirection,
|
||||
CallType,
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import {
|
||||
type ContextMenuTriggerType,
|
||||
MessageContextMenu,
|
||||
|
|
|
@ -11,7 +11,7 @@ import enMessages from '../../../_locales/en/messages.json';
|
|||
import type { PropsType as TimelineItemProps } from './TimelineItem';
|
||||
import { TimelineItem } from './TimelineItem';
|
||||
import { UniversalTimerNotification } from './UniversalTimerNotification';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import { CallMode } from '../../types/CallDisposition';
|
||||
import { AvatarColors } from '../../types/Colors';
|
||||
import { getDefaultConversation } from '../../test-both/helpers/getDefaultConversation';
|
||||
import { WidthBreakpoint } from '../_util';
|
||||
|
|
|
@ -19,12 +19,7 @@ import { makeFakeLookupConversationWithoutServiceId } from '../../../test-both/h
|
|||
import { ThemeType } from '../../../types/Util';
|
||||
import { DurationInSeconds } from '../../../util/durations';
|
||||
import { NavTab } from '../../../state/ducks/nav';
|
||||
import { CallMode } from '../../../types/Calling';
|
||||
import {
|
||||
CallDirection,
|
||||
CallType,
|
||||
DirectCallStatus,
|
||||
} from '../../../types/CallDisposition';
|
||||
import { getFakeCallHistoryGroup } from '../../../test-both/helpers/getFakeCallHistoryGroup';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -224,30 +219,15 @@ export const _11 = (): JSX.Element => (
|
|||
<ConversationDetails {...createProps()} isGroup={false} />
|
||||
);
|
||||
|
||||
function mins(n: number) {
|
||||
return DurationInSeconds.toMillis(DurationInSeconds.fromMinutes(n));
|
||||
}
|
||||
|
||||
export function WithCallHistoryGroup(): JSX.Element {
|
||||
const props = createProps();
|
||||
|
||||
return (
|
||||
<ConversationDetails
|
||||
{...props}
|
||||
callHistoryGroup={{
|
||||
peerId: props.conversation?.serviceId ?? '',
|
||||
mode: CallMode.Direct,
|
||||
type: CallType.Video,
|
||||
direction: CallDirection.Incoming,
|
||||
status: DirectCallStatus.Accepted,
|
||||
timestamp: Date.now(),
|
||||
children: [
|
||||
{ callId: '123', timestamp: Date.now() },
|
||||
{ callId: '122', timestamp: Date.now() - mins(30) },
|
||||
{ callId: '121', timestamp: Date.now() - mins(45) },
|
||||
{ callId: '121', timestamp: Date.now() - mins(60) },
|
||||
],
|
||||
}}
|
||||
callHistoryGroup={getFakeCallHistoryGroup({
|
||||
peerId: props.conversation?.serviceId,
|
||||
})}
|
||||
selectedNavTab={NavTab.Calls}
|
||||
/>
|
||||
);
|
||||
|
|
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 { drop } from '../util/drop';
|
||||
import { callLinksDeleteJobQueue } from './callLinksDeleteJobQueue';
|
||||
|
||||
import { conversationJobQueue } from './conversationJobQueue';
|
||||
import { groupAvatarJobQueue } from './groupAvatarJobQueue';
|
||||
|
@ -40,6 +41,7 @@ export function initializeAllJobQueues({
|
|||
// Other queues
|
||||
drop(removeStorageKeyJobQueue.streamJobs());
|
||||
drop(reportSpamJobQueue.streamJobs());
|
||||
drop(callLinksDeleteJobQueue.streamJobs());
|
||||
}
|
||||
|
||||
export async function shutdownAllJobQueues(): Promise<void> {
|
||||
|
@ -52,5 +54,6 @@ export async function shutdownAllJobQueues(): Promise<void> {
|
|||
viewOnceOpenJobQueue.shutdown(),
|
||||
removeStorageKeyJobQueue.shutdown(),
|
||||
reportSpamJobQueue.shutdown(),
|
||||
callLinksDeleteJobQueue.shutdown(),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -68,11 +68,11 @@ import type {
|
|||
PresentedSource,
|
||||
} from '../types/Calling';
|
||||
import {
|
||||
CallMode,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
ScreenShareStatus,
|
||||
} from '../types/Calling';
|
||||
import { CallMode, LocalCallEvent } from '../types/CallDisposition';
|
||||
import {
|
||||
findBestMatchingAudioDeviceIndex,
|
||||
findBestMatchingCameraId,
|
||||
|
@ -135,17 +135,16 @@ import {
|
|||
getCallDetailsForAdhocCall,
|
||||
} from '../util/callDisposition';
|
||||
import { isNormalNumber } from '../util/isNormalNumber';
|
||||
import { LocalCallEvent } from '../types/CallDisposition';
|
||||
import type { AciString, ServiceIdString } from '../types/ServiceId';
|
||||
import { isServiceIdString } from '../types/ServiceId';
|
||||
import { isInSystemContacts } from '../util/isInSystemContacts';
|
||||
import { toAdminKeyBytes } from '../util/callLinks';
|
||||
import {
|
||||
getRoomIdFromRootKey,
|
||||
getCallLinkAuthCredentialPresentation,
|
||||
toAdminKeyBytes,
|
||||
getRoomIdFromRootKey,
|
||||
callLinkRestrictionsToRingRTC,
|
||||
callLinkStateFromRingRTC,
|
||||
} from '../util/callLinks';
|
||||
} from '../util/callLinksRingrtc';
|
||||
import { isAdhocCallingEnabled } from '../util/isAdhocCallingEnabled';
|
||||
import {
|
||||
conversationJobQueue,
|
||||
|
@ -154,7 +153,10 @@ import {
|
|||
import type { CallLinkType, CallLinkStateType } from '../types/CallLink';
|
||||
import { CallLinkRestrictions } from '../types/CallLink';
|
||||
import { getConversationIdForLogging } from '../util/idForLogging';
|
||||
import { sendCallLinkUpdateSync } from '../util/sendCallLinkUpdateSync';
|
||||
import {
|
||||
sendCallLinkDeleteSync,
|
||||
sendCallLinkUpdateSync,
|
||||
} from '../util/sendCallLinkUpdateSync';
|
||||
import { createIdenticon } from '../util/createIdenticon';
|
||||
import { getColorForCallLink } from '../util/getColorForCallLink';
|
||||
|
||||
|
@ -683,6 +685,41 @@ export class CallingClass {
|
|||
return callLink;
|
||||
}
|
||||
|
||||
async deleteCallLink(callLink: CallLinkType): Promise<void> {
|
||||
strictAssert(
|
||||
this._sfuUrl,
|
||||
'createCallLink() missing SFU URL; not deleting call link'
|
||||
);
|
||||
|
||||
const sfuUrl = this._sfuUrl;
|
||||
const logId = `deleteCallLink(${callLink.roomId})`;
|
||||
|
||||
const callLinkRootKey = CallLinkRootKey.parse(callLink.rootKey);
|
||||
strictAssert(callLink.adminKey, 'Missing admin key');
|
||||
const callLinkAdminKey = toAdminKeyBytes(callLink.adminKey);
|
||||
const authCredentialPresentation =
|
||||
await getCallLinkAuthCredentialPresentation(callLinkRootKey);
|
||||
|
||||
const result = await RingRTC.deleteCallLink(
|
||||
sfuUrl,
|
||||
authCredentialPresentation.serialize(),
|
||||
callLinkRootKey,
|
||||
callLinkAdminKey
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
if (result.errorStatusCode === 404) {
|
||||
log.info(`${logId}: Call link not found, already deleted`);
|
||||
return;
|
||||
}
|
||||
const message = `Failed to delete call link: ${result.errorStatusCode}`;
|
||||
log.error(`${logId}: ${message}`);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
drop(sendCallLinkDeleteSync(callLink));
|
||||
}
|
||||
|
||||
async updateCallLinkName(
|
||||
callLink: CallLinkType,
|
||||
name: string
|
||||
|
|
|
@ -562,6 +562,7 @@ type ReadableInterface = {
|
|||
callLinkExists(roomId: string): boolean;
|
||||
getAllCallLinks: () => ReadonlyArray<CallLinkType>;
|
||||
getCallLinkByRoomId: (roomId: string) => CallLinkType | undefined;
|
||||
getAllMarkedDeletedCallLinks(): ReadonlyArray<CallLinkType>;
|
||||
getMessagesBetween: (
|
||||
conversationId: string,
|
||||
options: GetMessagesBetweenOptions
|
||||
|
@ -794,6 +795,10 @@ type WritableInterface = {
|
|||
roomId: string,
|
||||
callLinkState: CallLinkStateType
|
||||
): CallLinkType;
|
||||
beginDeleteAllCallLinks(): void;
|
||||
beginDeleteCallLink(roomId: string): void;
|
||||
finalizeDeleteCallLink(roomId: string): void;
|
||||
deleteCallLinkFromSync(roomId: string): void;
|
||||
migrateConversationMessages: (obsoleteId: string, currentId: string) => void;
|
||||
saveEditedMessage: (
|
||||
mainMessage: ReadonlyDeep<MessageType>,
|
||||
|
|
|
@ -168,6 +168,7 @@ import {
|
|||
GroupCallStatus,
|
||||
CallType,
|
||||
CallStatusValue,
|
||||
CallMode,
|
||||
} from '../types/CallDisposition';
|
||||
import {
|
||||
callLinkExists,
|
||||
|
@ -176,13 +177,17 @@ import {
|
|||
insertCallLink,
|
||||
updateCallLinkAdminKeyByRoomId,
|
||||
updateCallLinkState,
|
||||
beginDeleteAllCallLinks,
|
||||
getAllMarkedDeletedCallLinks,
|
||||
finalizeDeleteCallLink,
|
||||
beginDeleteCallLink,
|
||||
deleteCallLinkFromSync,
|
||||
} from './server/callLinks';
|
||||
import {
|
||||
replaceAllEndorsementsForGroup,
|
||||
deleteAllEndorsementsForGroup,
|
||||
getGroupSendCombinedEndorsementExpiration,
|
||||
} from './server/groupEndorsements';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import {
|
||||
attachmentDownloadJobSchema,
|
||||
type AttachmentDownloadJobType,
|
||||
|
@ -296,6 +301,7 @@ export const DataReader: ServerReadableInterface = {
|
|||
callLinkExists,
|
||||
getAllCallLinks,
|
||||
getCallLinkByRoomId,
|
||||
getAllMarkedDeletedCallLinks,
|
||||
getMessagesBetween,
|
||||
getNearbyMessageFromDeletedSet,
|
||||
getMostRecentAddressableMessages,
|
||||
|
@ -430,6 +436,10 @@ export const DataWriter: ServerWritableInterface = {
|
|||
insertCallLink,
|
||||
updateCallLinkAdminKeyByRoomId,
|
||||
updateCallLinkState,
|
||||
beginDeleteAllCallLinks,
|
||||
beginDeleteCallLink,
|
||||
finalizeDeleteCallLink,
|
||||
deleteCallLinkFromSync,
|
||||
migrateConversationMessages,
|
||||
saveEditedMessage,
|
||||
saveEditedMessages,
|
||||
|
@ -3458,7 +3468,7 @@ function clearCallHistory(
|
|||
return db.transaction(() => {
|
||||
const timestamp = getMessageTimestampForCallLogEventTarget(db, target);
|
||||
|
||||
const [selectCallIdsQuery, selectCallIdsParams] = sql`
|
||||
const [selectCallsQuery, selectCallsParams] = sql`
|
||||
SELECT callsHistory.callId
|
||||
FROM callsHistory
|
||||
WHERE
|
||||
|
@ -3471,18 +3481,30 @@ function clearCallHistory(
|
|||
);
|
||||
`;
|
||||
|
||||
const callIds = db
|
||||
.prepare(selectCallIdsQuery)
|
||||
const deletedCallIds: ReadonlyArray<string> = db
|
||||
.prepare(selectCallsQuery)
|
||||
.pluck()
|
||||
.all(selectCallIdsParams);
|
||||
.all(selectCallsParams);
|
||||
|
||||
let deletedMessageIds: ReadonlyArray<string> = [];
|
||||
|
||||
batchMultiVarQuery(db, callIds, (ids: ReadonlyArray<string>): void => {
|
||||
batchMultiVarQuery(db, deletedCallIds, (ids): void => {
|
||||
const idsFragment = sqlJoin(ids);
|
||||
|
||||
const [clearCallsHistoryQuery, clearCallsHistoryParams] = sql`
|
||||
UPDATE callsHistory
|
||||
SET
|
||||
status = ${DirectCallStatus.Deleted},
|
||||
timestamp = ${Date.now()}
|
||||
WHERE callsHistory.callId IN (${idsFragment});
|
||||
`;
|
||||
|
||||
db.prepare(clearCallsHistoryQuery).run(clearCallsHistoryParams);
|
||||
|
||||
const [deleteMessagesQuery, deleteMessagesParams] = sql`
|
||||
DELETE FROM messages
|
||||
WHERE messages.type IS 'call-history'
|
||||
AND messages.callId IN (${sqlJoin(ids)})
|
||||
AND messages.callId IN (${idsFragment})
|
||||
RETURNING id;
|
||||
`;
|
||||
|
||||
|
@ -3494,21 +3516,6 @@ function clearCallHistory(
|
|||
deletedMessageIds = deletedMessageIds.concat(batchDeletedMessageIds);
|
||||
});
|
||||
|
||||
const [clearCallsHistoryQuery, clearCallsHistoryParams] = sql`
|
||||
UPDATE callsHistory
|
||||
SET
|
||||
status = ${DirectCallStatus.Deleted},
|
||||
timestamp = ${Date.now()}
|
||||
WHERE callsHistory.timestamp <= ${timestamp};
|
||||
`;
|
||||
|
||||
try {
|
||||
db.prepare(clearCallsHistoryQuery).run(clearCallsHistoryParams);
|
||||
} catch (error) {
|
||||
logger.error(error, error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return deletedMessageIds;
|
||||
})();
|
||||
}
|
||||
|
@ -3519,9 +3526,8 @@ function markCallHistoryDeleted(db: WritableDB, callId: string): void {
|
|||
SET
|
||||
status = ${DirectCallStatus.Deleted},
|
||||
timestamp = ${Date.now()}
|
||||
WHERE callId = ${callId};
|
||||
WHERE callId = ${callId}
|
||||
`;
|
||||
|
||||
db.prepare(query).run(params);
|
||||
}
|
||||
|
||||
|
@ -4829,7 +4835,7 @@ function getNextAttachmentBackupJobs(
|
|||
active = 0
|
||||
AND
|
||||
(retryAfter is NULL OR retryAfter <= ${timestamp})
|
||||
ORDER BY
|
||||
ORDER BY
|
||||
-- type is "standard" or "thumbnail"; we prefer "standard" jobs
|
||||
type ASC, receivedAt DESC
|
||||
LIMIT ${limit}
|
||||
|
|
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,
|
||||
GroupCallStatus,
|
||||
callHistoryDetailsSchema,
|
||||
CallMode,
|
||||
} from '../../types/CallDisposition';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import type { WritableDB, MessageType, ConversationType } from '../Interface';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
|
|
|
@ -89,10 +89,11 @@ import { updateToSchemaVersion1090 } from './1090-message-delete-indexes';
|
|||
import { updateToSchemaVersion1100 } from './1100-optimize-mark-call-history-read-in-conversation';
|
||||
import { updateToSchemaVersion1110 } from './1110-sticker-local-key';
|
||||
import { updateToSchemaVersion1120 } from './1120-messages-foreign-keys-indexes';
|
||||
import { updateToSchemaVersion1130 } from './1130-isStory-index';
|
||||
import {
|
||||
updateToSchemaVersion1130,
|
||||
updateToSchemaVersion1140,
|
||||
version as MAX_VERSION,
|
||||
} from './1130-isStory-index';
|
||||
} from './1140-call-links-deleted-column';
|
||||
|
||||
function updateToSchemaVersion1(
|
||||
currentVersion: number,
|
||||
|
@ -2050,6 +2051,7 @@ export const SCHEMA_VERSIONS = [
|
|||
updateToSchemaVersion1110,
|
||||
updateToSchemaVersion1120,
|
||||
updateToSchemaVersion1130,
|
||||
updateToSchemaVersion1140,
|
||||
];
|
||||
|
||||
export class DBVersionFromFutureError extends Error {
|
||||
|
|
|
@ -7,15 +7,16 @@ import {
|
|||
callLinkRestrictionsSchema,
|
||||
callLinkRecordSchema,
|
||||
} from '../../types/CallLink';
|
||||
import { toAdminKeyBytes } from '../../util/callLinks';
|
||||
import {
|
||||
callLinkToRecord,
|
||||
callLinkFromRecord,
|
||||
toAdminKeyBytes,
|
||||
} from '../../util/callLinks';
|
||||
} from '../../util/callLinksRingrtc';
|
||||
import type { ReadableDB, WritableDB } from '../Interface';
|
||||
import { prepare } from '../Server';
|
||||
import { sql } from '../util';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { CallStatusValue } from '../../types/CallDisposition';
|
||||
|
||||
export function callLinkExists(db: ReadableDB, roomId: string): boolean {
|
||||
const [query, params] = sql`
|
||||
|
@ -133,3 +134,106 @@ function assertRoomIdMatchesRootKey(roomId: string, rootKey: string): void {
|
|||
'passed roomId must match roomId derived from root key'
|
||||
);
|
||||
}
|
||||
|
||||
function deleteCallHistoryByRoomId(db: WritableDB, roomId: string) {
|
||||
const [
|
||||
markCallHistoryDeleteByPeerIdQuery,
|
||||
markCallHistoryDeleteByPeerIdParams,
|
||||
] = sql`
|
||||
UPDATE callsHistory
|
||||
SET
|
||||
status = ${CallStatusValue.Deleted},
|
||||
timestamp = ${Date.now()}
|
||||
WHERE peerId = ${roomId}
|
||||
`;
|
||||
|
||||
db.prepare(markCallHistoryDeleteByPeerIdQuery).run(
|
||||
markCallHistoryDeleteByPeerIdParams
|
||||
);
|
||||
}
|
||||
|
||||
// This should only be called from a sync message to avoid accidentally deleting
|
||||
// on the client but not the server
|
||||
export function deleteCallLinkFromSync(db: WritableDB, roomId: string): void {
|
||||
db.transaction(() => {
|
||||
const [query, params] = sql`
|
||||
DELETE FROM callLinks
|
||||
WHERE roomId = ${roomId};
|
||||
`;
|
||||
|
||||
db.prepare(query).run(params);
|
||||
|
||||
deleteCallHistoryByRoomId(db, roomId);
|
||||
})();
|
||||
}
|
||||
|
||||
export function beginDeleteCallLink(db: WritableDB, roomId: string): void {
|
||||
db.transaction(() => {
|
||||
// If adminKey is null, then we should delete the call link
|
||||
const [deleteNonAdminCallLinksQuery, deleteNonAdminCallLinksParams] = sql`
|
||||
DELETE FROM callLinks
|
||||
WHERE adminKey IS NULL
|
||||
AND roomId = ${roomId};
|
||||
`;
|
||||
|
||||
const result = db
|
||||
.prepare(deleteNonAdminCallLinksQuery)
|
||||
.run(deleteNonAdminCallLinksParams);
|
||||
|
||||
// Skip this query if the call is already deleted
|
||||
if (result.changes === 0) {
|
||||
// If the admin key is not null, we should mark it for deletion
|
||||
const [markAdminCallLinksDeletedQuery, markAdminCallLinksDeletedParams] =
|
||||
sql`
|
||||
UPDATE callLinks
|
||||
SET deleted = 1
|
||||
WHERE adminKey IS NOT NULL
|
||||
AND roomId = ${roomId};
|
||||
`;
|
||||
|
||||
db.prepare(markAdminCallLinksDeletedQuery).run(
|
||||
markAdminCallLinksDeletedParams
|
||||
);
|
||||
}
|
||||
|
||||
deleteCallHistoryByRoomId(db, roomId);
|
||||
})();
|
||||
}
|
||||
|
||||
export function beginDeleteAllCallLinks(db: WritableDB): void {
|
||||
db.transaction(() => {
|
||||
const [markAdminCallLinksDeletedQuery] = sql`
|
||||
UPDATE callLinks
|
||||
SET deleted = 1
|
||||
WHERE adminKey IS NOT NULL;
|
||||
`;
|
||||
|
||||
db.prepare(markAdminCallLinksDeletedQuery).run();
|
||||
|
||||
const [deleteNonAdminCallLinksQuery] = sql`
|
||||
DELETE FROM callLinks
|
||||
WHERE adminKey IS NULL;
|
||||
`;
|
||||
|
||||
db.prepare(deleteNonAdminCallLinksQuery).run();
|
||||
})();
|
||||
}
|
||||
|
||||
export function getAllMarkedDeletedCallLinks(
|
||||
db: ReadableDB
|
||||
): ReadonlyArray<CallLinkType> {
|
||||
const [query] = sql`
|
||||
SELECT * FROM callLinks WHERE deleted = 1;
|
||||
`;
|
||||
return db
|
||||
.prepare(query)
|
||||
.all()
|
||||
.map(item => callLinkFromRecord(callLinkRecordSchema.parse(item)));
|
||||
}
|
||||
|
||||
export function finalizeDeleteCallLink(db: WritableDB, roomId: string): void {
|
||||
const [query, params] = sql`
|
||||
DELETE FROM callLinks WHERE roomId = ${roomId} AND deleted = 1;
|
||||
`;
|
||||
db.prepare(query).run(params);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,12 @@ import {
|
|||
getCallHistoryLatestCall,
|
||||
getCallHistorySelector,
|
||||
} from '../selectors/callHistory';
|
||||
import {
|
||||
getCallsHistoryForRedux,
|
||||
getCallsHistoryUnreadCountForRedux,
|
||||
loadCallsHistory,
|
||||
} from '../../services/callHistoryLoader';
|
||||
import { makeLookup } from '../../util/makeLookup';
|
||||
|
||||
export type CallHistoryState = ReadonlyDeep<{
|
||||
// This informs the app that underlying call history data has changed.
|
||||
|
@ -34,6 +40,7 @@ export type CallHistoryState = ReadonlyDeep<{
|
|||
const CALL_HISTORY_ADD = 'callHistory/ADD';
|
||||
const CALL_HISTORY_REMOVE = 'callHistory/REMOVE';
|
||||
const CALL_HISTORY_RESET = 'callHistory/RESET';
|
||||
const CALL_HISTORY_RELOAD = 'callHistory/RELOAD';
|
||||
const CALL_HISTORY_UPDATE_UNREAD = 'callHistory/UPDATE_UNREAD';
|
||||
|
||||
export type CallHistoryAdd = ReadonlyDeep<{
|
||||
|
@ -50,6 +57,14 @@ export type CallHistoryReset = ReadonlyDeep<{
|
|||
type: typeof CALL_HISTORY_RESET;
|
||||
}>;
|
||||
|
||||
export type CallHistoryReload = ReadonlyDeep<{
|
||||
type: typeof CALL_HISTORY_RELOAD;
|
||||
payload: {
|
||||
callsHistory: ReadonlyArray<CallHistoryDetails>;
|
||||
callsHistoryUnreadCount: number;
|
||||
};
|
||||
}>;
|
||||
|
||||
export type CallHistoryUpdateUnread = ReadonlyDeep<{
|
||||
type: typeof CALL_HISTORY_UPDATE_UNREAD;
|
||||
payload: number;
|
||||
|
@ -59,6 +74,7 @@ export type CallHistoryAction = ReadonlyDeep<
|
|||
| CallHistoryAdd
|
||||
| CallHistoryRemove
|
||||
| CallHistoryReset
|
||||
| CallHistoryReload
|
||||
| CallHistoryUpdateUnread
|
||||
>;
|
||||
|
||||
|
@ -178,9 +194,29 @@ function clearAllCallHistory(): ThunkAction<
|
|||
} catch (error) {
|
||||
log.error('Error clearing call history', Errors.toLogFormat(error));
|
||||
} finally {
|
||||
// Just force a reset, even if the clear failed.
|
||||
dispatch(resetCallHistory());
|
||||
dispatch(updateCallHistoryUnreadCount());
|
||||
// Just force a reload, even if the clear failed.
|
||||
dispatch(reloadCallHistory());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function reloadCallHistory(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
CallHistoryReload
|
||||
> {
|
||||
return async dispatch => {
|
||||
try {
|
||||
await loadCallsHistory();
|
||||
const callsHistory = getCallsHistoryForRedux();
|
||||
const callsHistoryUnreadCount = getCallsHistoryUnreadCountForRedux();
|
||||
dispatch({
|
||||
type: CALL_HISTORY_RELOAD,
|
||||
payload: { callsHistory, callsHistoryUnreadCount },
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('Error reloading call history', Errors.toLogFormat(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -226,6 +262,12 @@ export function reducer(
|
|||
...state,
|
||||
unreadCount: action.payload,
|
||||
};
|
||||
case CALL_HISTORY_RELOAD:
|
||||
return {
|
||||
edition: state.edition + 1,
|
||||
unreadCount: action.payload.callsHistoryUnreadCount,
|
||||
callHistoryByCallId: makeLookup(action.payload.callsHistory, 'callId'),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -41,21 +41,21 @@ import {
|
|||
MAX_CALLING_REACTIONS,
|
||||
CallEndedReason,
|
||||
CallingDeviceType,
|
||||
CallMode,
|
||||
CallViewMode,
|
||||
CallState,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
} from '../../types/Calling';
|
||||
import { CallMode } from '../../types/CallDisposition';
|
||||
import { callingTones } from '../../util/callingTones';
|
||||
import { requestCameraPermissions } from '../../util/callingPermissions';
|
||||
import {
|
||||
CALL_LINK_DEFAULT_STATE,
|
||||
getRoomIdFromRootKey,
|
||||
isCallLinksCreateEnabled,
|
||||
toAdminKeyBytes,
|
||||
toCallHistoryFromUnusedCallLink,
|
||||
} from '../../util/callLinks';
|
||||
import { getRoomIdFromRootKey } from '../../util/callLinksRingrtc';
|
||||
import { sendCallLinkUpdateSync } from '../../util/sendCallLinkUpdateSync';
|
||||
import { sleep } from '../../util/sleep';
|
||||
import { LatestQueue } from '../../util/LatestQueue';
|
||||
|
@ -92,10 +92,11 @@ import { getConversationIdForLogging } from '../../util/idForLogging';
|
|||
import { DataReader, DataWriter } from '../../sql/Client';
|
||||
import { isAciString } from '../../util/isAciString';
|
||||
import type { CallHistoryAdd } from './callHistory';
|
||||
import { addCallHistory } from './callHistory';
|
||||
import { addCallHistory, reloadCallHistory } from './callHistory';
|
||||
import { saveDraftRecordingIfNeeded } from './composer';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import type { StartCallData } from '../../components/ConfirmLeaveCallModal';
|
||||
import { callLinksDeleteJobQueue } from '../../jobs/callLinksDeleteJobQueue';
|
||||
import { getCallLinksByRoomId } from '../selectors/calling';
|
||||
|
||||
// State
|
||||
|
@ -255,6 +256,10 @@ type HandleCallLinkUpdateActionPayloadType = ReadonlyDeep<{
|
|||
callLink: CallLinkType;
|
||||
}>;
|
||||
|
||||
type HandleCallLinkDeleteActionPayloadType = ReadonlyDeep<{
|
||||
roomId: string;
|
||||
}>;
|
||||
|
||||
type HangUpActionPayloadType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
}>;
|
||||
|
@ -264,6 +269,10 @@ export type HandleCallLinkUpdateType = ReadonlyDeep<{
|
|||
adminKey: string | null;
|
||||
}>;
|
||||
|
||||
export type HandleCallLinkDeleteType = ReadonlyDeep<{
|
||||
roomId: string;
|
||||
}>;
|
||||
|
||||
export type IncomingDirectCallType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
isVideoCall: boolean;
|
||||
|
@ -598,6 +607,7 @@ const GROUP_CALL_STATE_CHANGE = 'calling/GROUP_CALL_STATE_CHANGE';
|
|||
const GROUP_CALL_REACTIONS_RECEIVED = 'calling/GROUP_CALL_REACTIONS_RECEIVED';
|
||||
const GROUP_CALL_REACTIONS_EXPIRED = 'calling/GROUP_CALL_REACTIONS_EXPIRED';
|
||||
const HANDLE_CALL_LINK_UPDATE = 'calling/HANDLE_CALL_LINK_UPDATE';
|
||||
const HANDLE_CALL_LINK_DELETE = 'calling/HANDLE_CALL_LINK_DELETE';
|
||||
const HANG_UP = 'calling/HANG_UP';
|
||||
const INCOMING_DIRECT_CALL = 'calling/INCOMING_DIRECT_CALL';
|
||||
const INCOMING_GROUP_CALL = 'calling/INCOMING_GROUP_CALL';
|
||||
|
@ -740,10 +750,15 @@ type GroupCallReactionsExpiredActionType = ReadonlyDeep<{
|
|||
}>;
|
||||
|
||||
type HandleCallLinkUpdateActionType = ReadonlyDeep<{
|
||||
type: 'calling/HANDLE_CALL_LINK_UPDATE';
|
||||
type: typeof HANDLE_CALL_LINK_UPDATE;
|
||||
payload: HandleCallLinkUpdateActionPayloadType;
|
||||
}>;
|
||||
|
||||
type HandleCallLinkDeleteActionType = ReadonlyDeep<{
|
||||
type: typeof HANDLE_CALL_LINK_DELETE;
|
||||
payload: HandleCallLinkDeleteActionPayloadType;
|
||||
}>;
|
||||
|
||||
type HangUpActionType = ReadonlyDeep<{
|
||||
type: 'calling/HANG_UP';
|
||||
payload: HangUpActionPayloadType;
|
||||
|
@ -903,6 +918,7 @@ export type CallingActionType =
|
|||
| GroupCallReactionsReceivedActionType
|
||||
| GroupCallReactionsExpiredActionType
|
||||
| HandleCallLinkUpdateActionType
|
||||
| HandleCallLinkDeleteActionType
|
||||
| HangUpActionType
|
||||
| IncomingDirectCallActionType
|
||||
| IncomingGroupCallActionType
|
||||
|
@ -1466,6 +1482,19 @@ function handleCallLinkUpdate(
|
|||
};
|
||||
}
|
||||
|
||||
function handleCallLinkDelete(
|
||||
payload: HandleCallLinkDeleteType
|
||||
): ThunkAction<void, RootStateType, unknown, HandleCallLinkDeleteActionType> {
|
||||
return async dispatch => {
|
||||
dispatch({
|
||||
type: HANDLE_CALL_LINK_DELETE,
|
||||
payload,
|
||||
});
|
||||
|
||||
dispatch(reloadCallHistory());
|
||||
};
|
||||
}
|
||||
|
||||
function hangUpActiveCall(
|
||||
reason: string
|
||||
): ThunkAction<void, RootStateType, unknown, HangUpActionType> {
|
||||
|
@ -1971,6 +2000,16 @@ function createCallLink(
|
|||
};
|
||||
}
|
||||
|
||||
function deleteCallLink(
|
||||
roomId: string
|
||||
): ThunkAction<void, RootStateType, unknown, HandleCallLinkDeleteActionType> {
|
||||
return async dispatch => {
|
||||
await DataWriter.beginDeleteCallLink(roomId);
|
||||
await callLinksDeleteJobQueue.add({ source: 'deleteCallLink' });
|
||||
dispatch(handleCallLinkDelete({ roomId }));
|
||||
};
|
||||
}
|
||||
|
||||
function updateCallLinkName(
|
||||
roomId: string,
|
||||
name: string
|
||||
|
@ -2394,6 +2433,7 @@ export const actions = {
|
|||
closeNeedPermissionScreen,
|
||||
createCallLink,
|
||||
declineCall,
|
||||
deleteCallLink,
|
||||
denyUser,
|
||||
getPresentingSources,
|
||||
groupCallAudioLevelsChange,
|
||||
|
@ -2402,6 +2442,7 @@ export const actions = {
|
|||
groupCallStateChange,
|
||||
hangUpActiveCall,
|
||||
handleCallLinkUpdate,
|
||||
handleCallLinkDelete,
|
||||
joinedAdhocCall,
|
||||
leaveCurrentCallAndStartCallingLobby,
|
||||
onOutgoingVideoCallInConversation,
|
||||
|
|
|
@ -3,11 +3,8 @@
|
|||
|
||||
// Note that this file should not important any binary addons or Node.js modules
|
||||
// because it can be imported by storybook
|
||||
import {
|
||||
CallMode,
|
||||
CallState,
|
||||
GroupCallConnectionState,
|
||||
} from '../../types/Calling';
|
||||
import { CallState, GroupCallConnectionState } from '../../types/Calling';
|
||||
import { CallMode } from '../../types/CallDisposition';
|
||||
import type { AciString } from '../../types/ServiceId';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import type {
|
||||
|
|
|
@ -70,7 +70,7 @@ import type {
|
|||
DraftBodyRanges,
|
||||
HydratedBodyRangesType,
|
||||
} from '../../types/BodyRange';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import { CallMode } from '../../types/CallDisposition';
|
||||
import type { MediaItemType } from '../../types/MediaItem';
|
||||
import type { StoryDistributionIdString } from '../../types/StoryDistributionId';
|
||||
import { normalizeStoryDistributionId } from '../../types/StoryDistributionId';
|
||||
|
|
|
@ -13,7 +13,7 @@ import type {
|
|||
GroupCallStateType,
|
||||
} from '../ducks/calling';
|
||||
import { getIncomingCall as getIncomingCallHelper } from '../ducks/callingHelpers';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import { CallMode } from '../../types/CallDisposition';
|
||||
import type { CallLinkType } from '../../types/CallLink';
|
||||
import { getUserACI } from './user';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
|
|
|
@ -144,8 +144,7 @@ import {
|
|||
} from '../../util/getTitle';
|
||||
import { getMessageSentTimestamp } from '../../util/getMessageSentTimestamp';
|
||||
import type { CallHistorySelectorType } from './callHistory';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import { CallDirection } from '../../types/CallDisposition';
|
||||
import { CallMode, CallDirection } from '../../types/CallDisposition';
|
||||
import { getCallIdFromEra } from '../../util/callDisposition';
|
||||
import { LONG_MESSAGE } from '../../types/MIME';
|
||||
import type { MessageRequestResponseNotificationData } from '../../components/conversation/MessageRequestResponseNotification';
|
||||
|
|
|
@ -9,10 +9,8 @@ import { getIntl } from '../selectors/user';
|
|||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { getCallLinkAddNameModalRoomId } from '../selectors/globalModals';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import {
|
||||
isCallLinkAdmin,
|
||||
isCallLinksCreateEnabled,
|
||||
} from '../../util/callLinks';
|
||||
import { isCallLinksCreateEnabled } from '../../util/callLinks';
|
||||
import { isCallLinkAdmin } from '../../types/CallLink';
|
||||
import { CallLinkAddNameModal } from '../../components/CallLinkAddNameModal';
|
||||
|
||||
export const SmartCallLinkAddNameModal = memo(
|
||||
|
|
|
@ -15,21 +15,30 @@ import type { CallLinkRestrictions } from '../../types/CallLink';
|
|||
export type SmartCallLinkDetailsProps = Readonly<{
|
||||
roomId: string;
|
||||
callHistoryGroup: CallHistoryGroup;
|
||||
onClose: () => void;
|
||||
}>;
|
||||
|
||||
export const SmartCallLinkDetails = memo(function SmartCallLinkDetails({
|
||||
roomId,
|
||||
callHistoryGroup,
|
||||
onClose,
|
||||
}: SmartCallLinkDetailsProps) {
|
||||
const i18n = useSelector(getIntl);
|
||||
const callLinkSelector = useSelector(getCallLinkSelector);
|
||||
const { startCallLinkLobby, updateCallLinkRestrictions } =
|
||||
|
||||
const { deleteCallLink, startCallLinkLobby, updateCallLinkRestrictions } =
|
||||
useCallingActions();
|
||||
const { toggleCallLinkAddNameModal, showShareCallLinkViaSignal } =
|
||||
useGlobalModalActions();
|
||||
|
||||
const callLink = callLinkSelector(roomId);
|
||||
|
||||
const handleDeleteCallLink = useCallback(() => {
|
||||
strictAssert(callLink != null, 'callLink not found');
|
||||
deleteCallLink(callLink.roomId);
|
||||
onClose();
|
||||
}, [callLink, deleteCallLink, onClose]);
|
||||
|
||||
const handleOpenCallLinkAddNameModal = useCallback(() => {
|
||||
toggleCallLinkAddNameModal(roomId);
|
||||
}, [roomId, toggleCallLinkAddNameModal]);
|
||||
|
@ -61,6 +70,7 @@ export const SmartCallLinkDetails = memo(function SmartCallLinkDetails({
|
|||
callHistoryGroup={callHistoryGroup}
|
||||
callLink={callLink}
|
||||
i18n={i18n}
|
||||
onDeleteCallLink={handleDeleteCallLink}
|
||||
onOpenCallLinkAddNameModal={handleOpenCallLinkAddNameModal}
|
||||
onStartCallLinkLobby={handleStartCallLinkLobby}
|
||||
onShareCallLinkViaSignal={handleShareCallLinkViaSignal}
|
||||
|
|
|
@ -32,7 +32,8 @@ import type {
|
|||
ConversationsByDemuxIdType,
|
||||
GroupCallRemoteParticipantType,
|
||||
} from '../../types/Calling';
|
||||
import { CallMode, CallState } from '../../types/Calling';
|
||||
import { CallState } from '../../types/Calling';
|
||||
import { CallMode } from '../../types/CallDisposition';
|
||||
import type { AciString } from '../../types/ServiceId';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { callLinkToConversation } from '../../util/callLinks';
|
||||
|
|
|
@ -107,10 +107,15 @@ function getCallHistoryFilter({
|
|||
|
||||
function renderCallLinkDetails(
|
||||
roomId: string,
|
||||
callHistoryGroup: CallHistoryGroup
|
||||
callHistoryGroup: CallHistoryGroup,
|
||||
onClose: () => void
|
||||
): JSX.Element {
|
||||
return (
|
||||
<SmartCallLinkDetails roomId={roomId} callHistoryGroup={callHistoryGroup} />
|
||||
<SmartCallLinkDetails
|
||||
roomId={roomId}
|
||||
callHistoryGroup={callHistoryGroup}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -167,11 +172,8 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
startCallLinkLobbyByRoomId,
|
||||
togglePip,
|
||||
} = useCallingActions();
|
||||
const {
|
||||
clearAllCallHistory: clearCallHistory,
|
||||
markCallHistoryRead,
|
||||
markCallsTabViewed,
|
||||
} = useCallHistoryActions();
|
||||
const { clearAllCallHistory, markCallHistoryRead, markCallsTabViewed } =
|
||||
useCallHistoryActions();
|
||||
const { toggleCallLinkEditModal, toggleConfirmLeaveCallModal } =
|
||||
useGlobalModalActions();
|
||||
|
||||
|
@ -244,7 +246,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
hasPendingUpdate={hasPendingUpdate}
|
||||
i18n={i18n}
|
||||
navTabsCollapsed={navTabsCollapsed}
|
||||
onClearCallHistory={clearCallHistory}
|
||||
onClearCallHistory={clearAllCallHistory}
|
||||
onMarkCallHistoryRead={markCallHistoryRead}
|
||||
onToggleNavTabsCollapse={toggleNavTabsCollapse}
|
||||
onCreateCallLink={handleCreateCallLink}
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from '../../components/conversation/ConversationHeader';
|
||||
import { getCannotLeaveBecauseYouAreLastAdmin } from '../../components/conversation/conversation-details/ConversationDetails';
|
||||
import { useMinimalConversation } from '../../hooks/useMinimalConversation';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import { CallMode } from '../../types/CallDisposition';
|
||||
import { PanelType } from '../../types/Panels';
|
||||
import { StoryViewModeType } from '../../types/Stories';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
|
|
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 { callLinkToRecord, callLinkFromRecord } from '../../util/callLinks';
|
||||
import {
|
||||
callLinkToRecord,
|
||||
callLinkFromRecord,
|
||||
} from '../../util/callLinksRingrtc';
|
||||
import {
|
||||
FAKE_CALL_LINK as CALL_LINK,
|
||||
FAKE_CALL_LINK_WITH_ADMIN_KEY as CALL_LINK_WITH_ADMIN_KEY,
|
||||
|
|
|
@ -3,18 +3,18 @@
|
|||
|
||||
import { assert } from 'chai';
|
||||
import { getCallingNotificationText } from '../../util/callingNotification';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import {
|
||||
CallMode,
|
||||
CallDirection,
|
||||
CallType,
|
||||
GroupCallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import {
|
||||
getDefaultConversation,
|
||||
getDefaultGroup,
|
||||
} from '../helpers/getDefaultConversation';
|
||||
import {
|
||||
CallDirection,
|
||||
CallType,
|
||||
GroupCallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import { getPeerIdFromConversation } from '../../util/callDisposition';
|
||||
import { HOUR } from '../../util/durations';
|
||||
|
||||
|
|
|
@ -6,7 +6,14 @@ import { v4 as generateUuid } from 'uuid';
|
|||
|
||||
import { DataReader, DataWriter } from '../../sql/Client';
|
||||
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import {
|
||||
CallMode,
|
||||
AdhocCallStatus,
|
||||
CallDirection,
|
||||
CallHistoryFilterStatus,
|
||||
CallType,
|
||||
DirectCallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import { generateAci } from '../../types/ServiceId';
|
||||
import type { ServiceIdString } from '../../types/ServiceId';
|
||||
import type {
|
||||
|
@ -14,13 +21,6 @@ import type {
|
|||
CallHistoryGroup,
|
||||
CallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import {
|
||||
AdhocCallStatus,
|
||||
CallDirection,
|
||||
CallHistoryFilterStatus,
|
||||
CallType,
|
||||
DirectCallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import type { ConversationAttributesType } from '../../model-types';
|
||||
import {
|
||||
|
|
|
@ -7,14 +7,14 @@ import { v4 as generateUuid } from 'uuid';
|
|||
import { times } from 'lodash';
|
||||
import { DataReader, DataWriter } from '../../sql/Client';
|
||||
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import { generateAci } from '../../types/ServiceId';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import {
|
||||
CallMode,
|
||||
CallDirection,
|
||||
CallType,
|
||||
GroupCallStatus,
|
||||
} from '../../types/CallDisposition';
|
||||
import { generateAci } from '../../types/ServiceId';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import type { MaybeStaleCallHistory } from '../../sql/Server';
|
||||
|
||||
const { getAllCallHistory } = DataReader;
|
||||
|
|
|
@ -30,12 +30,12 @@ import { isAnybodyElseInGroupCall } from '../../../state/ducks/callingHelpers';
|
|||
import { truncateAudioLevel } from '../../../calling/truncateAudioLevel';
|
||||
import { calling as callingService } from '../../../services/calling';
|
||||
import {
|
||||
CallMode,
|
||||
CallState,
|
||||
CallViewMode,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
} from '../../../types/Calling';
|
||||
import { CallMode } from '../../../types/CallDisposition';
|
||||
import { generateAci } from '../../../types/ServiceId';
|
||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
import type { UnwrapPromise } from '../../../types/Util';
|
||||
|
|
|
@ -36,7 +36,7 @@ import {
|
|||
} from '../../../state/ducks/conversations';
|
||||
import { ReadStatus } from '../../../messages/MessageReadStatus';
|
||||
import type { SingleServePromiseIdString } from '../../../services/singleServePromise';
|
||||
import { CallMode } from '../../../types/Calling';
|
||||
import { CallMode } from '../../../types/CallDisposition';
|
||||
import { generateAci, getAciFromPrefix } from '../../../types/ServiceId';
|
||||
import { generateStoryDistributionId } from '../../../types/StoryDistributionId';
|
||||
import {
|
||||
|
|
|
@ -6,12 +6,12 @@ import { reducer as rootReducer } from '../../../state/reducer';
|
|||
import { noopAction } from '../../../state/ducks/noop';
|
||||
import { actions as userActions } from '../../../state/ducks/user';
|
||||
import {
|
||||
CallMode,
|
||||
CallState,
|
||||
CallViewMode,
|
||||
GroupCallConnectionState,
|
||||
GroupCallJoinState,
|
||||
} from '../../../types/Calling';
|
||||
import { CallMode } from '../../../types/CallDisposition';
|
||||
import { generateAci } from '../../../types/ServiceId';
|
||||
import {
|
||||
getCallsByConversation,
|
||||
|
|
|
@ -6,8 +6,8 @@ import { findLast } from 'lodash';
|
|||
import type { WritableDB } from '../../sql/Interface';
|
||||
import { markAllCallHistoryRead } from '../../sql/Server';
|
||||
import { SeenStatus } from '../../MessageSeenStatus';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import {
|
||||
CallMode,
|
||||
CallDirection,
|
||||
CallType,
|
||||
DirectCallStatus,
|
||||
|
|
|
@ -5,15 +5,15 @@ import { assert } from 'chai';
|
|||
import { v4 as generateGuid } from 'uuid';
|
||||
|
||||
import { jsonToObject, sql } from '../../sql/util';
|
||||
import { CallMode } from '../../types/Calling';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import {
|
||||
CallMode,
|
||||
CallDirection,
|
||||
CallType,
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
callHistoryDetailsSchema,
|
||||
} from '../../types/CallDisposition';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import type {
|
||||
CallHistoryDetailsFromDiskType,
|
||||
MessageWithCallHistoryDetails,
|
||||
|
|
|
@ -95,12 +95,12 @@ import {
|
|||
AdhocCallStatus,
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
CallMode,
|
||||
} from '../types/CallDisposition';
|
||||
import {
|
||||
getBytesForPeerId,
|
||||
getProtoForCallHistory,
|
||||
} from '../util/callDisposition';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import { MAX_MESSAGE_COUNT } from '../util/deleteForMe.types';
|
||||
|
||||
export type SendMetadataType = {
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import { z } from 'zod';
|
||||
import Long from 'long';
|
||||
import { CallMode } from './Calling';
|
||||
import type { AciString } from './ServiceId';
|
||||
import { aciSchema } from './ServiceId';
|
||||
import { bytesToUuid } from '../util/uuidToBytes';
|
||||
|
@ -11,6 +10,13 @@ import { SignalService as Proto } from '../protobuf';
|
|||
import * as Bytes from '../Bytes';
|
||||
import { UUID_BYTE_SIZE } from './Crypto';
|
||||
|
||||
// These are strings (1) for the database (2) for Storybook.
|
||||
export enum CallMode {
|
||||
Direct = 'Direct',
|
||||
Group = 'Group',
|
||||
Adhoc = 'Adhoc',
|
||||
}
|
||||
|
||||
export enum CallType {
|
||||
Audio = 'Audio',
|
||||
Video = 'Video',
|
||||
|
|
|
@ -98,3 +98,7 @@ export const callLinkRecordSchema = z.object({
|
|||
expiration: z.number().int().nullable(),
|
||||
revoked: z.union([z.literal(1), z.literal(0)]),
|
||||
}) satisfies z.ZodType<CallLinkRecord>;
|
||||
|
||||
export function isCallLinkAdmin(callLink: CallLinkType): boolean {
|
||||
return callLink.adminKey != null;
|
||||
}
|
||||
|
|
|
@ -5,17 +5,10 @@ import type { AudioDevice, Reaction as CallReaction } from '@signalapp/ringrtc';
|
|||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { AciString, ServiceIdString } from './ServiceId';
|
||||
import type { CallLinkConversationType } from './CallLink';
|
||||
import type { CallMode } from './CallDisposition';
|
||||
|
||||
export const MAX_CALLING_REACTIONS = 5;
|
||||
export const CALLING_REACTIONS_LIFETIME = 4000;
|
||||
|
||||
// These are strings (1) for the database (2) for Storybook.
|
||||
export enum CallMode {
|
||||
Direct = 'Direct',
|
||||
Group = 'Group',
|
||||
Adhoc = 'Adhoc',
|
||||
}
|
||||
|
||||
// Speaker and Presentation mode have the same UI, but Presentation is only set
|
||||
// automatically when someone starts to present, and will revert to the previous view mode
|
||||
// once presentation is complete
|
||||
|
|
|
@ -18,11 +18,24 @@ import { DataReader, DataWriter } from '../sql/Client';
|
|||
import { SignalService as Proto } from '../protobuf';
|
||||
import { bytesToUuid, uuidToBytes } from './uuidToBytes';
|
||||
import { missingCaseError } from './missingCaseError';
|
||||
import { CallEndedReason, GroupCallJoinState } from '../types/Calling';
|
||||
import {
|
||||
CallEndedReason,
|
||||
CallMode,
|
||||
GroupCallJoinState,
|
||||
} from '../types/Calling';
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
callEventNormalizeSchema,
|
||||
CallType,
|
||||
CallDirection,
|
||||
callEventDetailsSchema,
|
||||
LocalCallEvent,
|
||||
RemoteCallEvent,
|
||||
callHistoryDetailsSchema,
|
||||
callDetailsSchema,
|
||||
AdhocCallStatus,
|
||||
CallStatusValue,
|
||||
callLogEventNormalizeSchema,
|
||||
CallLogEvent,
|
||||
} from '../types/CallDisposition';
|
||||
import type { AciString } from '../types/ServiceId';
|
||||
import { isAciString } from './isAciString';
|
||||
import { isMe } from './whatTypeOfConversation';
|
||||
|
@ -49,26 +62,11 @@ import type {
|
|||
CallStatus,
|
||||
GroupCallMeta,
|
||||
} from '../types/CallDisposition';
|
||||
import {
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
callEventNormalizeSchema,
|
||||
CallType,
|
||||
CallDirection,
|
||||
callEventDetailsSchema,
|
||||
LocalCallEvent,
|
||||
RemoteCallEvent,
|
||||
callHistoryDetailsSchema,
|
||||
callDetailsSchema,
|
||||
AdhocCallStatus,
|
||||
CallStatusValue,
|
||||
callLogEventNormalizeSchema,
|
||||
CallLogEvent,
|
||||
} from '../types/CallDisposition';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
import { drop } from './drop';
|
||||
import { sendCallLinkUpdateSync } from './sendCallLinkUpdateSync';
|
||||
import { callLinksDeleteJobQueue } from '../jobs/callLinksDeleteJobQueue';
|
||||
|
||||
// utils
|
||||
// -----
|
||||
|
@ -1295,11 +1293,15 @@ export async function clearCallHistoryDataAndSync(
|
|||
`clearCallHistory: Clearing call history before (${latestCall.callId}, ${latestCall.timestamp})`
|
||||
);
|
||||
const messageIds = await DataWriter.clearCallHistory(latestCall);
|
||||
await DataWriter.beginDeleteAllCallLinks();
|
||||
updateDeletedMessages(messageIds);
|
||||
log.info('clearCallHistory: Queueing sync message');
|
||||
await singleProtoJobQueue.add(
|
||||
MessageSender.getClearCallHistoryMessage(latestCall)
|
||||
);
|
||||
await callLinksDeleteJobQueue.add({
|
||||
source: 'clearCallHistoryDataAndSync',
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('clearCallHistory: Failed to clear call history', error);
|
||||
}
|
||||
|
|
|
@ -1,46 +1,20 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { CallLinkState as RingRTCCallLinkState } from '@signalapp/ringrtc';
|
||||
import {
|
||||
CallLinkRootKey,
|
||||
CallLinkRestrictions as RingRTCCallLinkRestrictions,
|
||||
} from '@signalapp/ringrtc';
|
||||
import { Aci } from '@signalapp/libsignal-client';
|
||||
import { z } from 'zod';
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
import * as RemoteConfig from '../RemoteConfig';
|
||||
import type { CallLinkAuthCredentialPresentation } from './zkgroup';
|
||||
import {
|
||||
CallLinkAuthCredential,
|
||||
CallLinkSecretParams,
|
||||
GenericServerPublicParams,
|
||||
} from './zkgroup';
|
||||
import { getCheckedCallLinkAuthCredentialsForToday } from '../services/groupCredentialFetcher';
|
||||
import * as durations from './durations';
|
||||
import * as Bytes from '../Bytes';
|
||||
import type {
|
||||
CallLinkConversationType,
|
||||
CallLinkType,
|
||||
CallLinkRecord,
|
||||
CallLinkStateType,
|
||||
} from '../types/CallLink';
|
||||
import {
|
||||
CallLinkNameMaxByteLength,
|
||||
callLinkRecordSchema,
|
||||
CallLinkRestrictions,
|
||||
toCallLinkRestrictions,
|
||||
} from '../types/CallLink';
|
||||
import type { CallLinkConversationType, CallLinkType } from '../types/CallLink';
|
||||
import { CallLinkRestrictions } from '../types/CallLink';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { isTestOrMockEnvironment } from '../environment';
|
||||
import { getColorForCallLink } from './getColorForCallLink';
|
||||
import { unicodeSlice } from './unicodeSlice';
|
||||
import {
|
||||
AdhocCallStatus,
|
||||
CallDirection,
|
||||
CallType,
|
||||
type CallHistoryDetails,
|
||||
CallMode,
|
||||
} from '../types/CallDisposition';
|
||||
import { CallMode } from '../types/Calling';
|
||||
|
||||
export const CALL_LINK_DEFAULT_STATE = {
|
||||
name: '',
|
||||
|
@ -56,49 +30,6 @@ export function isCallLinksCreateEnabled(): boolean {
|
|||
return RemoteConfig.getValue('desktop.calling.adhoc.create') === 'TRUE';
|
||||
}
|
||||
|
||||
export function getRoomIdFromRootKey(rootKey: CallLinkRootKey): string {
|
||||
return rootKey.deriveRoomId().toString('hex');
|
||||
}
|
||||
|
||||
export function getCallLinkRootKeyFromUrlKey(key: string): Uint8Array {
|
||||
// Returns `Buffer` which inherits from `Uint8Array`
|
||||
return CallLinkRootKey.parse(key).bytes;
|
||||
}
|
||||
|
||||
export async function getCallLinkAuthCredentialPresentation(
|
||||
callLinkRootKey: CallLinkRootKey
|
||||
): Promise<CallLinkAuthCredentialPresentation> {
|
||||
const credentials = getCheckedCallLinkAuthCredentialsForToday(
|
||||
'getCallLinkAuthCredentialPresentation'
|
||||
);
|
||||
const todaysCredentials = credentials.today.credential;
|
||||
const credential = new CallLinkAuthCredential(
|
||||
Buffer.from(todaysCredentials, 'base64')
|
||||
);
|
||||
|
||||
const genericServerPublicParamsBase64 = window.getGenericServerPublicParams();
|
||||
const genericServerPublicParams = new GenericServerPublicParams(
|
||||
Buffer.from(genericServerPublicParamsBase64, 'base64')
|
||||
);
|
||||
|
||||
const ourAci = window.textsecure.storage.user.getAci();
|
||||
if (ourAci == null) {
|
||||
throw new Error('Failed to get our ACI');
|
||||
}
|
||||
const userId = Aci.fromUuid(ourAci);
|
||||
|
||||
const callLinkSecretParams = CallLinkSecretParams.deriveFromRootKey(
|
||||
callLinkRootKey.bytes
|
||||
);
|
||||
const presentation = credential.present(
|
||||
userId,
|
||||
credentials.today.redemptionTime / durations.SECOND,
|
||||
genericServerPublicParams,
|
||||
callLinkSecretParams
|
||||
);
|
||||
return presentation;
|
||||
}
|
||||
|
||||
export function callLinkToConversation(
|
||||
callLink: CallLinkType,
|
||||
i18n: LocalizerType
|
||||
|
@ -131,14 +62,6 @@ export function getPlaceholderCallLinkConversation(
|
|||
};
|
||||
}
|
||||
|
||||
export function toRootKeyBytes(rootKey: string): Uint8Array {
|
||||
return CallLinkRootKey.parse(rootKey).bytes;
|
||||
}
|
||||
|
||||
export function fromRootKeyBytes(rootKey: Uint8Array): string {
|
||||
return CallLinkRootKey.fromBytes(rootKey as Buffer).toString();
|
||||
}
|
||||
|
||||
export function toAdminKeyBytes(adminKey: string): Buffer {
|
||||
return Buffer.from(adminKey, 'base64');
|
||||
}
|
||||
|
@ -147,78 +70,6 @@ export function fromAdminKeyBytes(adminKey: Uint8Array): string {
|
|||
return Bytes.toBase64(adminKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* RingRTC conversions
|
||||
*/
|
||||
|
||||
export function callLinkStateFromRingRTC(
|
||||
state: RingRTCCallLinkState
|
||||
): CallLinkStateType {
|
||||
return {
|
||||
name: unicodeSlice(state.name, 0, CallLinkNameMaxByteLength),
|
||||
restrictions: toCallLinkRestrictions(state.restrictions),
|
||||
revoked: state.revoked,
|
||||
expiration: state.expiration.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
const RingRTCCallLinkRestrictionsSchema = z.nativeEnum(
|
||||
RingRTCCallLinkRestrictions
|
||||
);
|
||||
|
||||
export function callLinkRestrictionsToRingRTC(
|
||||
restrictions: CallLinkRestrictions
|
||||
): RingRTCCallLinkRestrictions {
|
||||
return RingRTCCallLinkRestrictionsSchema.parse(restrictions);
|
||||
}
|
||||
|
||||
/**
|
||||
* DB record conversions
|
||||
*/
|
||||
|
||||
export function callLinkToRecord(callLink: CallLinkType): CallLinkRecord {
|
||||
if (callLink.rootKey == null) {
|
||||
throw new Error('CallLink.callLinkToRecord: rootKey is null');
|
||||
}
|
||||
|
||||
const rootKey = toRootKeyBytes(callLink.rootKey);
|
||||
const adminKey = callLink.adminKey
|
||||
? toAdminKeyBytes(callLink.adminKey)
|
||||
: null;
|
||||
return callLinkRecordSchema.parse({
|
||||
roomId: callLink.roomId,
|
||||
rootKey,
|
||||
adminKey,
|
||||
name: callLink.name,
|
||||
restrictions: callLink.restrictions,
|
||||
revoked: callLink.revoked ? 1 : 0,
|
||||
expiration: callLink.expiration,
|
||||
});
|
||||
}
|
||||
|
||||
export function callLinkFromRecord(record: CallLinkRecord): CallLinkType {
|
||||
if (record.rootKey == null) {
|
||||
throw new Error('CallLink.callLinkFromRecord: rootKey is null');
|
||||
}
|
||||
|
||||
// root keys in memory are strings for simplicity
|
||||
const rootKey = fromRootKeyBytes(record.rootKey);
|
||||
const adminKey = record.adminKey ? fromAdminKeyBytes(record.adminKey) : null;
|
||||
return {
|
||||
roomId: record.roomId,
|
||||
rootKey,
|
||||
adminKey,
|
||||
name: record.name,
|
||||
restrictions: toCallLinkRestrictions(record.restrictions),
|
||||
revoked: record.revoked === 1,
|
||||
expiration: record.expiration,
|
||||
};
|
||||
}
|
||||
|
||||
export function isCallLinkAdmin(callLink: CallLinkType): boolean {
|
||||
return callLink.adminKey != null;
|
||||
}
|
||||
|
||||
export function toCallHistoryFromUnusedCallLink(
|
||||
callLink: CallLinkType
|
||||
): CallHistoryDetails {
|
||||
|
|
150
ts/util/callLinksRingrtc.ts
Normal file
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
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import {
|
||||
CallMode,
|
||||
CallState,
|
||||
GroupCallConnectionState,
|
||||
} from '../types/Calling';
|
||||
import { CallState, GroupCallConnectionState } from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import type { ActiveCallType } from '../types/Calling';
|
||||
import { isGroupOrAdhocActiveCall } from './isGroupOrAdhocCall';
|
||||
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import { missingCaseError } from './missingCaseError';
|
||||
import type { CallStatus } from '../types/CallDisposition';
|
||||
import {
|
||||
CallMode,
|
||||
CallDirection,
|
||||
DirectCallStatus,
|
||||
type CallHistoryDetails,
|
||||
CallType,
|
||||
GroupCallStatus,
|
||||
} from '../types/CallDisposition';
|
||||
import { missingCaseError } from './missingCaseError';
|
||||
import type { CallStatus } from '../types/CallDisposition';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { strictAssert } from './assert';
|
||||
import { isMoreRecentThan } from './timestamp';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { CallMode } from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
import type { ActiveCallType, ActiveGroupCallType } from '../types/Calling';
|
||||
import type {
|
||||
DirectCallStateType,
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
peerIdToLog,
|
||||
updateCallHistoryFromRemoteEvent,
|
||||
} from './callDisposition';
|
||||
import { CallMode } from '../types/Calling';
|
||||
import { CallMode } from '../types/CallDisposition';
|
||||
|
||||
export async function onCallEventSync(
|
||||
syncEvent: CallEventSyncEvent
|
||||
|
|
|
@ -5,9 +5,11 @@ import { CallLinkRootKey } from '@signalapp/ringrtc';
|
|||
import type { CallLinkUpdateSyncEvent } from '../textsecure/messageReceiverEvents';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
import { fromAdminKeyBytes, getRoomIdFromRootKey } from './callLinks';
|
||||
import { fromAdminKeyBytes } from './callLinks';
|
||||
import { getRoomIdFromRootKey } from './callLinksRingrtc';
|
||||
import { strictAssert } from './assert';
|
||||
import { CallLinkUpdateSyncType } from '../types/CallLink';
|
||||
import { DataWriter } from '../sql/Client';
|
||||
|
||||
export async function onCallLinkUpdateSync(
|
||||
syncEvent: CallLinkUpdateSyncEvent
|
||||
|
@ -46,8 +48,9 @@ export async function onCallLinkUpdateSync(
|
|||
adminKey: adminKeyString,
|
||||
});
|
||||
} else if (type === CallLinkUpdateSyncType.Delete) {
|
||||
// TODO: DESKTOP-6951
|
||||
log.warn(`${logId}: Deleting call links is not supported`);
|
||||
log.info(`${logId}: Deleting call link record ${roomId}`);
|
||||
await DataWriter.deleteCallLinkFromSync(roomId);
|
||||
window.reduxActions.calling.handleCallLinkDelete({ roomId });
|
||||
}
|
||||
|
||||
confirm();
|
||||
|
|
|
@ -7,7 +7,8 @@ import * as Errors from '../types/errors';
|
|||
import { SignalService as Proto } from '../protobuf';
|
||||
import { singleProtoJobQueue } from '../jobs/singleProtoJobQueue';
|
||||
import MessageSender from '../textsecure/SendMessage';
|
||||
import { toAdminKeyBytes, toRootKeyBytes } from './callLinks';
|
||||
import { toAdminKeyBytes } from './callLinks';
|
||||
import { toRootKeyBytes } from './callLinksRingrtc';
|
||||
|
||||
export type sendCallLinkUpdateSyncCallLinkType = {
|
||||
rootKey: string;
|
||||
|
|
|
@ -386,11 +386,11 @@ export const linkCallRoute = _route('linkCall', {
|
|||
},
|
||||
toWebUrl(args) {
|
||||
const params = new URLSearchParams({ key: args.key });
|
||||
return new URL(`https://signal.link/call#${params.toString()}`);
|
||||
return new URL(`https://signal.link/call/#${params.toString()}`);
|
||||
},
|
||||
toAppUrl(args) {
|
||||
const params = new URLSearchParams({ key: args.key });
|
||||
return new URL(`sgnl://signal.link/call#${params.toString()}`);
|
||||
return new URL(`sgnl://signal.link/call/#${params.toString()}`);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue