Init create/admin call links flow
This commit is contained in:
parent
53b8f5f152
commit
f19f0fb47d
31 changed files with 1256 additions and 149 deletions
|
@ -7080,6 +7080,10 @@
|
|||
"messageformat": "No results for “{query}”",
|
||||
"description": "Calls Tab > Calls List > When no results found > With a search query"
|
||||
},
|
||||
"icu:CallsList__CreateCallLink": {
|
||||
"messageformat": "Create a Call Link",
|
||||
"description": "Calls Tab > Calls List > Create Call Link Button"
|
||||
},
|
||||
"icu:CallsList__ItemCallInfo--Incoming": {
|
||||
"messageformat": "Incoming",
|
||||
"description": "Calls Tab > Calls List > Call Item > Call Status > When call was accepted and was incoming"
|
||||
|
@ -7164,6 +7168,30 @@
|
|||
"messageformat": "Share link via Signal",
|
||||
"description": "Call History > Call Link Details > Share Link via Signal Button"
|
||||
},
|
||||
"icu:CallLinkEditModal__Title": {
|
||||
"messageformat": "Call link details",
|
||||
"description": "Call Link Edit Modal > Title"
|
||||
},
|
||||
"icu:CallLinkEditModal__InputLabel--Name--SrOnly": {
|
||||
"messageformat": "Name",
|
||||
"description": "Call Link Edit Modal > Name Input > Label (for screenreaders)"
|
||||
},
|
||||
"icu:CallLinkEditModal__JoinButtonLabel": {
|
||||
"messageformat": "Join",
|
||||
"description": "Call Link Edit Modal > Join Button > Label"
|
||||
},
|
||||
"icu:CallLinkEditModal__InputLabel--ApproveAllMembers": {
|
||||
"messageformat": "Approve all members",
|
||||
"description": "Call Link Edit Modal > Approve All Members Checkbox > Label"
|
||||
},
|
||||
"icu:CallLinkEditModal__ApproveAllMembers__Option--Off": {
|
||||
"messageformat": "Off",
|
||||
"description": "Call Link Edit Modal > Approve All Members Checkbox > Option > Off"
|
||||
},
|
||||
"icu:CallLinkEditModal__ApproveAllMembers__Option--On": {
|
||||
"messageformat": "On",
|
||||
"description": "Call Link Edit Modal > Approve All Members Checkbox > Option > On"
|
||||
},
|
||||
"icu:TypingBubble__avatar--overflow-count": {
|
||||
"messageformat": "{count, plural, one {# other is} other {# others are}} typing.",
|
||||
"description": "Group chat multiple person typing indicator when space isn't available to show every avatar, this is the count of avatars hidden."
|
||||
|
|
|
@ -211,7 +211,7 @@
|
|||
"@formatjs/intl": "2.6.7",
|
||||
"@indutny/rezip-electron": "1.3.1",
|
||||
"@mixer/parallel-prettier": "2.0.3",
|
||||
"@signalapp/mock-server": "6.5.0",
|
||||
"@signalapp/mock-server": "6.6.0",
|
||||
"@storybook/addon-a11y": "7.4.5",
|
||||
"@storybook/addon-actions": "7.4.5",
|
||||
"@storybook/addon-controls": "7.4.5",
|
||||
|
|
139
stylesheets/components/CallLinkEditModal.scss
Normal file
139
stylesheets/components/CallLinkEditModal.scss
Normal file
|
@ -0,0 +1,139 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.CallLinkEditModal__SrOnly {
|
||||
@include sr-only;
|
||||
}
|
||||
|
||||
.CallLinkEditModal__Header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.CallLinkEditModal__Header__Details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 0; // fix overflow issue
|
||||
}
|
||||
|
||||
// Overriding default style
|
||||
.Input__container.CallLinkEditModal__Input--Name__container {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.CallLinkEditModal__CallLinkAndJoinButton {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.CallLinkEditModal__CopyUrlTextButton {
|
||||
@include button-reset;
|
||||
border: none;
|
||||
padding-block: 10px;
|
||||
padding-inline: 8px;
|
||||
border-radius: 6px;
|
||||
flex: 1;
|
||||
|
||||
// truncate with ellipsis
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@include light-theme {
|
||||
background: $color-gray-02;
|
||||
color: $color-black;
|
||||
}
|
||||
@include dark-theme {
|
||||
background: $color-gray-75;
|
||||
color: $color-gray-15;
|
||||
}
|
||||
}
|
||||
|
||||
.CallLinkEditModal__JoinButton {
|
||||
@include font-body-1-bold;
|
||||
}
|
||||
|
||||
.CallLinkEditModal__ApproveAllMembers__Row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.CallLinkEditModal__ApproveAllMembers__Label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.CallLinkEditModal__ActionButton {
|
||||
@include button-reset;
|
||||
@include font-body-2;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding-block: 8px;
|
||||
width: 100%;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-black;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-15;
|
||||
}
|
||||
}
|
||||
|
||||
.CallLinkEditModal__ActionButton__Icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 9999px;
|
||||
|
||||
@include light-theme {
|
||||
background: $color-gray-05;
|
||||
}
|
||||
@include dark-theme {
|
||||
background: $color-gray-65;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.CallLinkEditModal__ActionButton__Icon--Copy {
|
||||
&::after {
|
||||
@include light-theme {
|
||||
@include color-svg('../images/icons/v3/copy/copy.svg', $color-gray-75);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg('../images/icons/v3/copy/copy.svg', $color-gray-15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.CallLinkEditModal__ActionButton__Icon--Share {
|
||||
&::after {
|
||||
@include light-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/forward/forward.svg',
|
||||
$color-gray-75
|
||||
);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg(
|
||||
'../images/icons/v3/forward/forward.svg',
|
||||
$color-gray-15
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -161,7 +161,7 @@
|
|||
@include NavTabs__Scroller;
|
||||
}
|
||||
|
||||
.CallsList__List--loading {
|
||||
.CallsList__List--disableScrolling {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
@import './components/CallingSelectPresentingSourcesModal.scss';
|
||||
@import './components/CallingToast.scss';
|
||||
@import './components/CallLinkDetails.scss';
|
||||
@import './components/CallLinkEditModal.scss';
|
||||
@import './components/CallingRaisedHandsList.scss';
|
||||
@import './components/CallingRaisedHandsToasts.scss';
|
||||
@import './components/CallingReactionsToasts.scss';
|
||||
|
|
|
@ -16,6 +16,7 @@ import { getCountryCode } from './types/PhoneNumber';
|
|||
|
||||
export type ConfigKeyType =
|
||||
| 'desktop.calling.adhoc'
|
||||
| 'desktop.calling.adhoc.create'
|
||||
| 'desktop.clientExpiration'
|
||||
| 'desktop.backup.credentialFetch'
|
||||
| 'desktop.deleteSync.send'
|
||||
|
|
32
ts/components/CallLinkEditModal.stories.tsx
Normal file
32
ts/components/CallLinkEditModal.stories.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { CallLinkEditModalProps } from './CallLinkEditModal';
|
||||
import { CallLinkEditModal } from './CallLinkEditModal';
|
||||
import type { ComponentMeta } from '../storybook/types';
|
||||
import { FAKE_CALL_LINK_WITH_ADMIN_KEY } from '../test-both/helpers/fakeCallLink';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/CallLinkEditModal',
|
||||
component: CallLinkEditModal,
|
||||
args: {
|
||||
i18n,
|
||||
callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY,
|
||||
onClose: action('onClose'),
|
||||
onCopyCallLink: action('onCopyCallLink'),
|
||||
onUpdateCallLinkName: action('onUpdateCallLinkName'),
|
||||
onUpdateCallLinkRestrictions: action('onUpdateCallLinkRestrictions'),
|
||||
onShareCallLinkViaSignal: action('onShareCallLinkViaSignal'),
|
||||
onStartCallLinkLobby: action('onStartCallLinkLobby'),
|
||||
},
|
||||
} satisfies ComponentMeta<CallLinkEditModalProps>;
|
||||
|
||||
export function Basic(args: CallLinkEditModalProps): JSX.Element {
|
||||
return <CallLinkEditModal {...args} />;
|
||||
}
|
214
ts/components/CallLinkEditModal.tsx
Normal file
214
ts/components/CallLinkEditModal.tsx
Normal file
|
@ -0,0 +1,214 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
import { Modal } from './Modal';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import {
|
||||
CallLinkRestrictions,
|
||||
toCallLinkRestrictions,
|
||||
type CallLinkType,
|
||||
} from '../types/CallLink';
|
||||
import { Input } from './Input';
|
||||
import { Select } from './Select';
|
||||
import { linkCallRoute } from '../util/signalRoutes';
|
||||
import { Button, ButtonSize, ButtonVariant } from './Button';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { formatUrlWithoutProtocol } from '../util/url';
|
||||
|
||||
export type CallLinkEditModalProps = {
|
||||
i18n: LocalizerType;
|
||||
callLink: CallLinkType;
|
||||
onClose: () => void;
|
||||
onCopyCallLink: () => void;
|
||||
onUpdateCallLinkName: (name: string) => void;
|
||||
onUpdateCallLinkRestrictions: (restrictions: CallLinkRestrictions) => void;
|
||||
onShareCallLinkViaSignal: () => void;
|
||||
onStartCallLinkLobby: () => void;
|
||||
};
|
||||
|
||||
export function CallLinkEditModal({
|
||||
i18n,
|
||||
callLink,
|
||||
onClose,
|
||||
onCopyCallLink,
|
||||
onUpdateCallLinkName,
|
||||
onUpdateCallLinkRestrictions,
|
||||
onShareCallLinkViaSignal,
|
||||
onStartCallLinkLobby,
|
||||
}: CallLinkEditModalProps): JSX.Element {
|
||||
const { name: savedName, restrictions: savedRestrictions } = callLink;
|
||||
|
||||
const [nameId] = useState(() => generateUuid());
|
||||
const [restrictionsId] = useState(() => generateUuid());
|
||||
|
||||
const [nameInput, setNameInput] = useState(savedName);
|
||||
const [restrictionsInput, setRestrictionsInput] = useState(savedRestrictions);
|
||||
|
||||
// We only want to use the default name "Signal Call" as a value if the user
|
||||
// modified the input and then chose that name. Doesn't revert when saved.
|
||||
const [nameTouched, setNameTouched] = useState(false);
|
||||
|
||||
const callLinkWebUrl = useMemo(() => {
|
||||
return formatUrlWithoutProtocol(
|
||||
linkCallRoute.toWebUrl({ key: callLink.rootKey })
|
||||
);
|
||||
}, [callLink.rootKey]);
|
||||
|
||||
const onSaveName = useCallback(
|
||||
(newName: string) => {
|
||||
if (!nameTouched) {
|
||||
return;
|
||||
}
|
||||
if (newName === savedName) {
|
||||
return;
|
||||
}
|
||||
onUpdateCallLinkName(newName);
|
||||
},
|
||||
[nameTouched, savedName, onUpdateCallLinkName]
|
||||
);
|
||||
|
||||
const onSaveRestrictions = useCallback(
|
||||
(newRestrictions: CallLinkRestrictions) => {
|
||||
if (newRestrictions === savedRestrictions) {
|
||||
return;
|
||||
}
|
||||
onUpdateCallLinkRestrictions(newRestrictions);
|
||||
},
|
||||
[savedRestrictions, onUpdateCallLinkRestrictions]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
i18n={i18n}
|
||||
modalName="CallLinkEditModal"
|
||||
moduleClassName="CallLinkEditModal"
|
||||
title={i18n('icu:CallLinkEditModal__Title')}
|
||||
hasXButton
|
||||
onClose={() => {
|
||||
// Save the modal in case the user hits escape
|
||||
onSaveName(nameInput);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<div className="CallLinkEditModal__Header">
|
||||
<Avatar
|
||||
i18n={i18n}
|
||||
badge={undefined}
|
||||
conversationType="callLink"
|
||||
size={AvatarSize.SIXTY_FOUR}
|
||||
acceptedMessageRequest
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
title={callLink.name ?? i18n('icu:calling__call-link-default-title')}
|
||||
/>
|
||||
<div className="CallLinkEditModal__Header__Details">
|
||||
<label htmlFor={nameId} className="CallLinkEditModal__SrOnly">
|
||||
{i18n('icu:CallLinkEditModal__InputLabel--Name--SrOnly')}
|
||||
</label>
|
||||
<Input
|
||||
moduleClassName="CallLinkEditModal__Input--Name"
|
||||
i18n={i18n}
|
||||
value={
|
||||
nameInput === '' && !nameTouched
|
||||
? i18n('icu:calling__call-link-default-title')
|
||||
: nameInput
|
||||
}
|
||||
maxByteCount={120}
|
||||
onChange={value => {
|
||||
setNameTouched(true);
|
||||
setNameInput(value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
onSaveName(nameInput);
|
||||
}}
|
||||
onEnter={() => {
|
||||
onSaveName(nameInput);
|
||||
}}
|
||||
placeholder={i18n('icu:calling__call-link-default-title')}
|
||||
/>
|
||||
|
||||
<div className="CallLinkEditModal__CallLinkAndJoinButton">
|
||||
<button
|
||||
className="CallLinkEditModal__CopyUrlTextButton"
|
||||
type="button"
|
||||
onClick={onCopyCallLink}
|
||||
aria-label={i18n('icu:CallLinkDetails__CopyLink')}
|
||||
>
|
||||
{callLinkWebUrl}
|
||||
</button>
|
||||
<Button
|
||||
onClick={onStartCallLinkLobby}
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
className="CallLinkEditModal__JoinButton"
|
||||
>
|
||||
{i18n('icu:CallLinkEditModal__JoinButtonLabel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="CallLinkEditModal__ApproveAllMembers__Row"
|
||||
// For testing, to easily check the restrictions saved
|
||||
data-restrictions={savedRestrictions}
|
||||
>
|
||||
<label
|
||||
htmlFor={restrictionsId}
|
||||
className="CallLinkEditModal__ApproveAllMembers__Label"
|
||||
>
|
||||
{i18n('icu:CallLinkEditModal__InputLabel--ApproveAllMembers')}
|
||||
</label>
|
||||
<Select
|
||||
id={restrictionsId}
|
||||
value={restrictionsInput}
|
||||
options={[
|
||||
{
|
||||
value: CallLinkRestrictions.None,
|
||||
text: i18n(
|
||||
'icu:CallLinkEditModal__ApproveAllMembers__Option--Off'
|
||||
),
|
||||
},
|
||||
{
|
||||
value: CallLinkRestrictions.AdminApproval,
|
||||
text: i18n(
|
||||
'icu:CallLinkEditModal__ApproveAllMembers__Option--On'
|
||||
),
|
||||
},
|
||||
]}
|
||||
onChange={value => {
|
||||
const newRestrictions = toCallLinkRestrictions(value);
|
||||
setRestrictionsInput(newRestrictions);
|
||||
onSaveRestrictions(newRestrictions);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="CallLinkEditModal__ActionButton"
|
||||
onClick={onCopyCallLink}
|
||||
>
|
||||
<i
|
||||
role="presentation"
|
||||
className="CallLinkEditModal__ActionButton__Icon CallLinkEditModal__ActionButton__Icon--Copy"
|
||||
/>
|
||||
{i18n('icu:CallLinkDetails__CopyLink')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="CallLinkEditModal__ActionButton"
|
||||
onClick={onShareCallLinkViaSignal}
|
||||
>
|
||||
<i
|
||||
role="presentation"
|
||||
className="CallLinkEditModal__ActionButton__Icon CallLinkEditModal__ActionButton__Icon--Share"
|
||||
/>
|
||||
{i18n('icu:CallLinkDetails__ShareLinkViaSignal')}
|
||||
</button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -127,6 +127,7 @@ const defaultPendingState: SearchState = {
|
|||
|
||||
type CallsListProps = Readonly<{
|
||||
activeCall: ActiveCallStateType | undefined;
|
||||
canCreateCallLinks: boolean;
|
||||
getCallHistoryGroupsCount: (
|
||||
options: CallHistoryFilterOptions
|
||||
) => Promise<number>;
|
||||
|
@ -142,6 +143,7 @@ type CallsListProps = Readonly<{
|
|||
hangUpActiveCall: (reason: string) => void;
|
||||
i18n: LocalizerType;
|
||||
selectedCallHistoryGroup: CallHistoryGroup | null;
|
||||
onCreateCallLink: () => void;
|
||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
||||
onChangeCallsTabSelectedView: (selectedView: CallsTabSelectedView) => void;
|
||||
|
@ -157,10 +159,6 @@ const INACTIVE_CALL_LINK_PEEK_INTERVAL = 5 * MINUTE;
|
|||
const PEEK_BATCH_COUNT = 10;
|
||||
const PEEK_QUEUE_INTERVAL = 30 * SECOND;
|
||||
|
||||
function rowHeight() {
|
||||
return CALL_LIST_ITEM_ROW_HEIGHT;
|
||||
}
|
||||
|
||||
function isSameOptions(
|
||||
a: CallHistoryFilterOptions,
|
||||
b: CallHistoryFilterOptions
|
||||
|
@ -168,8 +166,12 @@ function isSameOptions(
|
|||
return a.query === b.query && a.status === b.status;
|
||||
}
|
||||
|
||||
type SpecialRows = 'CreateCallLink' | 'EmptyState';
|
||||
type Row = CallHistoryGroup | SpecialRows;
|
||||
|
||||
export function CallsList({
|
||||
activeCall,
|
||||
canCreateCallLinks,
|
||||
getCallHistoryGroupsCount,
|
||||
getCallHistoryGroups,
|
||||
callHistoryEdition,
|
||||
|
@ -180,6 +182,7 @@ export function CallsList({
|
|||
hangUpActiveCall,
|
||||
i18n,
|
||||
selectedCallHistoryGroup,
|
||||
onCreateCallLink,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
onChangeCallsTabSelectedView,
|
||||
|
@ -190,7 +193,7 @@ export function CallsList({
|
|||
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
||||
const listRef = useRef<List>(null);
|
||||
const [queryInput, setQueryInput] = useState('');
|
||||
const [status, setStatus] = useState(CallHistoryFilterStatus.All);
|
||||
const [statusInput, setStatusInput] = useState(CallHistoryFilterStatus.All);
|
||||
const [searchState, setSearchState] = useState(defaultInitState);
|
||||
const [isLeaveCallDialogVisible, setIsLeaveCallDialogVisible] =
|
||||
useState(false);
|
||||
|
@ -200,6 +203,27 @@ export function CallsList({
|
|||
const getCallHistoryGroupsCountRef = useRef(getCallHistoryGroupsCount);
|
||||
const getCallHistoryGroupsRef = useRef(getCallHistoryGroups);
|
||||
|
||||
const searchStateQuery = searchState.options?.query ?? '';
|
||||
const searchStateStatus =
|
||||
searchState.options?.status ?? CallHistoryFilterStatus.All;
|
||||
const searchFiltering =
|
||||
searchStateQuery !== '' ||
|
||||
searchStateStatus !== CallHistoryFilterStatus.All;
|
||||
const searchPending = searchState.state === 'pending';
|
||||
|
||||
const rows = useMemo(() => {
|
||||
let results: ReadonlyArray<Row> = searchState.results?.items ?? [];
|
||||
if (results.length === 0) {
|
||||
results = ['EmptyState'];
|
||||
}
|
||||
if (!searchFiltering && canCreateCallLinks) {
|
||||
results = ['CreateCallLink', ...results];
|
||||
}
|
||||
return results;
|
||||
}, [searchState.results?.items, searchFiltering, canCreateCallLinks]);
|
||||
|
||||
const rowCount = rows.length;
|
||||
|
||||
const searchStateItemsRef = useRef<ReadonlyArray<CallHistoryGroup> | null>(
|
||||
null
|
||||
);
|
||||
|
@ -208,17 +232,14 @@ export function CallsList({
|
|||
new Map()
|
||||
);
|
||||
const inactiveCallLinksPeekedAtRef = useRef<Map<string, number>>(new Map());
|
||||
|
||||
const peekQueueTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
function clearPeekQueueTimer() {
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (peekQueueTimerRef.current != null) {
|
||||
clearInterval(peekQueueTimerRef.current);
|
||||
peekQueueTimerRef.current = null;
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearPeekQueueTimer();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -489,7 +510,7 @@ export function CallsList({
|
|||
async function search() {
|
||||
const options: CallHistoryFilterOptions = {
|
||||
query: queryInput.toLowerCase().normalize().trim(),
|
||||
status,
|
||||
status: statusInput,
|
||||
};
|
||||
|
||||
let timer = setTimeout(() => {
|
||||
|
@ -560,7 +581,7 @@ export function CallsList({
|
|||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [queryInput, status, callHistoryEdition, enqueueCallPeeks]);
|
||||
}, [queryInput, statusInput, callHistoryEdition, enqueueCallPeeks]);
|
||||
|
||||
const loadMoreRows = useCallback(
|
||||
async (props: IndexRange) => {
|
||||
|
@ -625,9 +646,72 @@ export function CallsList({
|
|||
[searchState]
|
||||
);
|
||||
|
||||
const rowHeight = useCallback(
|
||||
({ index }: Index) => {
|
||||
const item = rows.at(index) ?? null;
|
||||
|
||||
if (item === 'EmptyState') {
|
||||
// arbitary large number so the empty state can be as big as it wants,
|
||||
// scrolling should always be locked when the list is empty
|
||||
return 9999;
|
||||
}
|
||||
|
||||
return CALL_LIST_ITEM_ROW_HEIGHT;
|
||||
},
|
||||
[rows]
|
||||
);
|
||||
|
||||
const rowRenderer = useCallback(
|
||||
({ key, index, style }: ListRowProps) => {
|
||||
const item = searchState.results?.items.at(index) ?? null;
|
||||
const item = rows.at(index) ?? null;
|
||||
|
||||
if (item === 'CreateCallLink') {
|
||||
return (
|
||||
<div key={key} style={style}>
|
||||
<ListTile
|
||||
moduleClassName="CallsList__ItemTile"
|
||||
title={
|
||||
<span className="CallsList__ItemTitle">
|
||||
{i18n('icu:CallsList__CreateCallLink')}
|
||||
</span>
|
||||
}
|
||||
leading={
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
conversationType="callLink"
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
title=""
|
||||
sharedGroupNames={[]}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
badge={undefined}
|
||||
className="CallsList__ItemAvatar"
|
||||
/>
|
||||
}
|
||||
onClick={onCreateCallLink}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (item === 'EmptyState') {
|
||||
return (
|
||||
<div key={key} className="CallsList__EmptyState" style={style}>
|
||||
{searchStateQuery === '' ? (
|
||||
i18n('icu:CallsList__EmptyState--noQuery')
|
||||
) : (
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:CallsList__EmptyState--hasQuery"
|
||||
components={{
|
||||
query: <UserText text={searchStateQuery} />,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const conversation = getConversationForItem(item);
|
||||
const activeCallConversationId = activeCall?.conversationId;
|
||||
|
||||
|
@ -647,11 +731,7 @@ export function CallsList({
|
|||
);
|
||||
const isActiveVisible = Boolean(isCallButtonVisible && item && isActive);
|
||||
|
||||
if (
|
||||
searchState.state === 'pending' ||
|
||||
item == null ||
|
||||
conversation == null
|
||||
) {
|
||||
if (searchPending || item == null || conversation == null) {
|
||||
return (
|
||||
<div key={key} style={style}>
|
||||
<ListTile
|
||||
|
@ -697,6 +777,7 @@ export function CallsList({
|
|||
<div
|
||||
key={key}
|
||||
style={style}
|
||||
data-type={item.type}
|
||||
className={classNames('CallsList__Item', {
|
||||
'CallsList__Item--selected': isSelected,
|
||||
'CallsList__Item--missed': wasMissed,
|
||||
|
@ -792,13 +873,16 @@ export function CallsList({
|
|||
},
|
||||
[
|
||||
activeCall,
|
||||
searchState,
|
||||
rows,
|
||||
searchStateQuery,
|
||||
searchPending,
|
||||
getCallLink,
|
||||
getConversationForItem,
|
||||
getIsCallActive,
|
||||
getIsInCall,
|
||||
selectedCallHistoryGroup,
|
||||
onChangeCallsTabSelectedView,
|
||||
onCreateCallLink,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
startCallLinkLobbyByRoomId,
|
||||
|
@ -819,18 +903,13 @@ export function CallsList({
|
|||
}, []);
|
||||
|
||||
const handleStatusToggle = useCallback(() => {
|
||||
setStatus(prevStatus => {
|
||||
setStatusInput(prevStatus => {
|
||||
return prevStatus === CallHistoryFilterStatus.All
|
||||
? CallHistoryFilterStatus.Missed
|
||||
: CallHistoryFilterStatus.All;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const filteringByMissed = status === CallHistoryFilterStatus.Missed;
|
||||
|
||||
const hasEmptyResults = searchState.results?.count === 0;
|
||||
const currentQuery = searchState.options?.query ?? '';
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLeaveCallDialogVisible && (
|
||||
|
@ -874,10 +953,11 @@ export function CallsList({
|
|||
>
|
||||
<button
|
||||
className={classNames('CallsList__ToggleFilterByMissed', {
|
||||
'CallsList__ToggleFilterByMissed--pressed': filteringByMissed,
|
||||
'CallsList__ToggleFilterByMissed--pressed':
|
||||
statusInput === CallHistoryFilterStatus.Missed,
|
||||
})}
|
||||
type="button"
|
||||
aria-pressed={filteringByMissed}
|
||||
aria-pressed={statusInput === CallHistoryFilterStatus.Missed}
|
||||
aria-roledescription={i18n(
|
||||
'icu:CallsList__ToggleFilterByMissed__RoleDescription'
|
||||
)}
|
||||
|
@ -890,22 +970,6 @@ export function CallsList({
|
|||
</Tooltip>
|
||||
</NavSidebarSearchHeader>
|
||||
|
||||
{hasEmptyResults && (
|
||||
<p className="CallsList__EmptyState">
|
||||
{currentQuery === '' ? (
|
||||
i18n('icu:CallsList__EmptyState--noQuery')
|
||||
) : (
|
||||
<I18n
|
||||
i18n={i18n}
|
||||
id="icu:CallsList__EmptyState--hasQuery"
|
||||
components={{
|
||||
query: <UserText text={currentQuery} />,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<SizeObserver>
|
||||
{(ref, size) => {
|
||||
return (
|
||||
|
@ -915,7 +979,7 @@ export function CallsList({
|
|||
ref={infiniteLoaderRef}
|
||||
isRowLoaded={isRowLoaded}
|
||||
loadMoreRows={loadMoreRows}
|
||||
rowCount={searchState.results?.count}
|
||||
rowCount={rowCount}
|
||||
minimumBatchSize={100}
|
||||
threshold={30}
|
||||
>
|
||||
|
@ -923,13 +987,14 @@ export function CallsList({
|
|||
return (
|
||||
<List
|
||||
className={classNames('CallsList__List', {
|
||||
'CallsList__List--loading':
|
||||
searchState.state === 'pending',
|
||||
'CallsList__List--disableScrolling':
|
||||
searchState.results == null ||
|
||||
searchState.results.count === 0,
|
||||
})}
|
||||
ref={refMerger(listRef, registerChild)}
|
||||
width={size.width}
|
||||
height={size.height}
|
||||
rowCount={searchState.results?.count ?? 0}
|
||||
rowCount={rowCount}
|
||||
rowHeight={rowHeight}
|
||||
rowRenderer={rowRenderer}
|
||||
onRowsRendered={onRowsRendered}
|
||||
|
|
|
@ -41,6 +41,7 @@ type CallsTabProps = Readonly<{
|
|||
pagination: CallHistoryPagination
|
||||
) => Promise<Array<CallHistoryGroup>>;
|
||||
callHistoryEdition: number;
|
||||
canCreateCallLinks: boolean;
|
||||
getAdhocCall: (roomId: string) => CallStateType | undefined;
|
||||
getCall: (id: string) => CallStateType | undefined;
|
||||
getCallLink: (id: string) => CallLinkType | undefined;
|
||||
|
@ -53,6 +54,7 @@ type CallsTabProps = Readonly<{
|
|||
onClearCallHistory: () => void;
|
||||
onMarkCallHistoryRead: (conversationId: string, callId: string) => void;
|
||||
onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
|
||||
onCreateCallLink: () => void;
|
||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
||||
peekNotConnectedGroupCall: (options: PeekNotConnectedGroupCallType) => void;
|
||||
|
@ -93,6 +95,7 @@ export function CallsTab({
|
|||
getCallHistoryGroupsCount,
|
||||
getCallHistoryGroups,
|
||||
callHistoryEdition,
|
||||
canCreateCallLinks,
|
||||
getAdhocCall,
|
||||
getCall,
|
||||
getCallLink,
|
||||
|
@ -105,6 +108,7 @@ export function CallsTab({
|
|||
onClearCallHistory,
|
||||
onMarkCallHistoryRead,
|
||||
onToggleNavTabsCollapse,
|
||||
onCreateCallLink,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
peekNotConnectedGroupCall,
|
||||
|
@ -257,6 +261,7 @@ export function CallsTab({
|
|||
<CallsList
|
||||
key={CallsTabSidebarView.CallsListView}
|
||||
activeCall={activeCall}
|
||||
canCreateCallLinks={canCreateCallLinks}
|
||||
getCallHistoryGroupsCount={getCallHistoryGroupsCount}
|
||||
getCallHistoryGroups={getCallHistoryGroups}
|
||||
callHistoryEdition={callHistoryEdition}
|
||||
|
@ -268,6 +273,7 @@ export function CallsTab({
|
|||
i18n={i18n}
|
||||
selectedCallHistoryGroup={selectedView?.callHistoryGroup ?? null}
|
||||
onChangeCallsTabSelectedView={updateSelectedView}
|
||||
onCreateCallLink={onCreateCallLink}
|
||||
onOutgoingAudioCallInConversation={
|
||||
handleOutgoingAudioCallInConversation
|
||||
}
|
||||
|
|
|
@ -29,6 +29,9 @@ export type PropsType = {
|
|||
// AddUserToAnotherGroupModal
|
||||
addUserToAnotherGroupModalContactId: string | undefined;
|
||||
renderAddUserToAnotherGroup: () => JSX.Element;
|
||||
// CallLinkEditModal
|
||||
callLinkEditModalRoomId: string | null;
|
||||
renderCallLinkEditModal: () => JSX.Element;
|
||||
// ContactModal
|
||||
contactModalState: ContactModalStateType | undefined;
|
||||
renderContactModal: () => JSX.Element;
|
||||
|
@ -102,6 +105,9 @@ export function GlobalModalContainer({
|
|||
// AddUserToAnotherGroupModal
|
||||
addUserToAnotherGroupModalContactId,
|
||||
renderAddUserToAnotherGroup,
|
||||
// CallLinkEditModal
|
||||
callLinkEditModalRoomId,
|
||||
renderCallLinkEditModal,
|
||||
// ContactModal
|
||||
contactModalState,
|
||||
renderContactModal,
|
||||
|
@ -164,7 +170,8 @@ export function GlobalModalContainer({
|
|||
// We want the following dialogs to show in this order:
|
||||
// 1. Errors
|
||||
// 2. Safety Number Changes
|
||||
// 3. The Rest (in no particular order, but they're ordered alphabetically)
|
||||
// 3. Forward Modal, so other modals can open it
|
||||
// 4. The Rest (in no particular order, but they're ordered alphabetically)
|
||||
|
||||
// Errors
|
||||
if (errorModalProps) {
|
||||
|
@ -176,12 +183,21 @@ export function GlobalModalContainer({
|
|||
return renderSendAnywayDialog();
|
||||
}
|
||||
|
||||
// Forward Modal
|
||||
if (forwardMessagesProps) {
|
||||
return renderForwardMessagesModal();
|
||||
}
|
||||
|
||||
// The Rest
|
||||
|
||||
if (addUserToAnotherGroupModalContactId) {
|
||||
return renderAddUserToAnotherGroup();
|
||||
}
|
||||
|
||||
if (callLinkEditModalRoomId) {
|
||||
return renderCallLinkEditModal();
|
||||
}
|
||||
|
||||
if (editHistoryMessages) {
|
||||
return renderEditHistoryMessagesModal();
|
||||
}
|
||||
|
@ -194,10 +210,6 @@ export function GlobalModalContainer({
|
|||
return renderDeleteMessagesModal();
|
||||
}
|
||||
|
||||
if (forwardMessagesProps) {
|
||||
return renderForwardMessagesModal();
|
||||
}
|
||||
|
||||
if (messageRequestActionsConfirmationProps) {
|
||||
return renderMessageRequestActionsConfirmation();
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ export type PropsType = {
|
|||
maxLengthCount?: number;
|
||||
moduleClassName?: string;
|
||||
onChange: (value: string) => unknown;
|
||||
onBlur?: () => unknown;
|
||||
onEnter?: () => unknown;
|
||||
placeholder: string;
|
||||
value?: string;
|
||||
|
@ -76,6 +77,7 @@ export const Input = forwardRef<
|
|||
maxLengthCount = 0,
|
||||
moduleClassName,
|
||||
onChange,
|
||||
onBlur,
|
||||
onEnter,
|
||||
placeholder,
|
||||
value = '',
|
||||
|
@ -214,6 +216,7 @@ export const Input = forwardRef<
|
|||
id,
|
||||
spellCheck: !disableSpellcheck,
|
||||
onChange: handleChange,
|
||||
onBlur,
|
||||
onKeyDown: handleKeyDown,
|
||||
onPaste: handlePaste,
|
||||
placeholder,
|
||||
|
|
|
@ -45,6 +45,13 @@ import { uniqBy, noop, compact } from 'lodash';
|
|||
|
||||
import Long from 'long';
|
||||
import type { CallLinkAuthCredentialPresentation } from '@signalapp/libsignal-client/zkgroup';
|
||||
import {
|
||||
CallLinkSecretParams,
|
||||
CreateCallLinkCredentialRequestContext,
|
||||
CreateCallLinkCredentialResponse,
|
||||
GenericServerPublicParams,
|
||||
} from '@signalapp/libsignal-client/zkgroup';
|
||||
import { Aci } from '@signalapp/libsignal-client';
|
||||
import type {
|
||||
ActionsType as CallingReduxActionsType,
|
||||
GroupCallParticipantInfoType,
|
||||
|
@ -135,13 +142,20 @@ import {
|
|||
getRoomIdFromRootKey,
|
||||
getCallLinkAuthCredentialPresentation,
|
||||
toAdminKeyBytes,
|
||||
callLinkRestrictionsToRingRTC,
|
||||
callLinkStateFromRingRTC,
|
||||
} from '../util/callLinks';
|
||||
import { isAdhocCallingEnabled } from '../util/isAdhocCallingEnabled';
|
||||
import {
|
||||
conversationJobQueue,
|
||||
conversationQueueJobEnum,
|
||||
} from '../jobs/conversationJobQueue';
|
||||
import type { ReadCallLinkState } from '../types/CallLink';
|
||||
import type {
|
||||
CallLinkType,
|
||||
CallLinkStateType,
|
||||
ReadCallLinkState,
|
||||
} from '../types/CallLink';
|
||||
import { CallLinkRestrictions } from '../types/CallLink';
|
||||
|
||||
const {
|
||||
processGroupCallRingCancellation,
|
||||
|
@ -586,6 +600,162 @@ export class CallingClass {
|
|||
}
|
||||
}
|
||||
|
||||
async createCallLink(): Promise<CallLinkType> {
|
||||
strictAssert(
|
||||
this._sfuUrl,
|
||||
'createCallLink() missing SFU URL; not creating call link'
|
||||
);
|
||||
|
||||
const sfuUrl = this._sfuUrl;
|
||||
const userId = Aci.parseFromServiceIdString(
|
||||
window.textsecure.storage.user.getCheckedAci()
|
||||
);
|
||||
|
||||
const rootKey = CallLinkRootKey.generate();
|
||||
const roomId = rootKey.deriveRoomId();
|
||||
const roomIdHex = roomId.toString('hex');
|
||||
const logId = `createCallLink(${roomIdHex})`;
|
||||
|
||||
log.info(`${logId}: Creating call link`);
|
||||
|
||||
const adminKey = CallLinkRootKey.generateAdminPassKey();
|
||||
|
||||
const context = CreateCallLinkCredentialRequestContext.forRoomId(roomId);
|
||||
const requestBase64 = Bytes.toBase64(context.getRequest().serialize());
|
||||
|
||||
strictAssert(
|
||||
window.textsecure.messaging,
|
||||
'createCallLink(): We are offline'
|
||||
);
|
||||
const { credential: credentialBase64 } =
|
||||
await window.textsecure.messaging.server.callLinkCreateAuth(
|
||||
requestBase64
|
||||
);
|
||||
|
||||
const response = new CreateCallLinkCredentialResponse(
|
||||
Buffer.from(credentialBase64, 'base64')
|
||||
);
|
||||
|
||||
const genericServerPublicParams = new GenericServerPublicParams(
|
||||
Buffer.from(window.getGenericServerPublicParams(), 'base64')
|
||||
);
|
||||
const credential = context.receive(
|
||||
response,
|
||||
userId,
|
||||
genericServerPublicParams
|
||||
);
|
||||
|
||||
const secretParams = CallLinkSecretParams.deriveFromRootKey(rootKey.bytes);
|
||||
|
||||
const credentialPresentation = credential
|
||||
.present(roomId, userId, genericServerPublicParams, secretParams)
|
||||
.serialize();
|
||||
const serializedPublicParams = secretParams.getPublicParams().serialize();
|
||||
|
||||
const result = await RingRTC.createCallLink(
|
||||
sfuUrl,
|
||||
credentialPresentation,
|
||||
rootKey,
|
||||
adminKey,
|
||||
serializedPublicParams
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
const message = `Failed to create call link: ${result.errorStatusCode}`;
|
||||
log.error(`${logId}: ${message}`);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
log.info(`${logId}: success`);
|
||||
const state = callLinkStateFromRingRTC(result.value);
|
||||
|
||||
return {
|
||||
roomId: roomIdHex,
|
||||
rootKey: rootKey.toString(),
|
||||
adminKey: adminKey.toString('base64'),
|
||||
...state,
|
||||
};
|
||||
}
|
||||
|
||||
async updateCallLinkName(
|
||||
callLink: CallLinkType,
|
||||
name: string
|
||||
): Promise<CallLinkStateType> {
|
||||
strictAssert(
|
||||
this._sfuUrl,
|
||||
'updateCallLinkName() missing SFU URL; not update call link name'
|
||||
);
|
||||
const sfuUrl = this._sfuUrl;
|
||||
const logId = `updateCallLinkName(${callLink.roomId})`;
|
||||
|
||||
log.info(`${logId}: Updating call link name`);
|
||||
|
||||
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.updateCallLinkName(
|
||||
sfuUrl,
|
||||
authCredentialPresentation.serialize(),
|
||||
callLinkRootKey,
|
||||
callLinkAdminKey,
|
||||
name
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
const message = `Failed to update call link name: ${result.errorStatusCode}`;
|
||||
log.error(`${logId}: ${message}`);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
log.info(`${logId}: success`);
|
||||
return callLinkStateFromRingRTC(result.value);
|
||||
}
|
||||
|
||||
async updateCallLinkRestrictions(
|
||||
callLink: CallLinkType,
|
||||
restrictions: CallLinkRestrictions
|
||||
): Promise<CallLinkStateType> {
|
||||
strictAssert(
|
||||
this._sfuUrl,
|
||||
'updateCallLinkRestrictions() missing SFU URL; not update call link restrictions'
|
||||
);
|
||||
const sfuUrl = this._sfuUrl;
|
||||
const logId = `updateCallLinkRestrictions(${callLink.roomId})`;
|
||||
|
||||
log.info(`${logId}: Updating call link restrictions`);
|
||||
|
||||
const callLinkRootKey = CallLinkRootKey.parse(callLink.rootKey);
|
||||
strictAssert(callLink.adminKey, 'Missing admin key');
|
||||
const callLinkAdminKey = toAdminKeyBytes(callLink.adminKey);
|
||||
const authCredentialPresentation =
|
||||
await getCallLinkAuthCredentialPresentation(callLinkRootKey);
|
||||
|
||||
const newRestrictions = callLinkRestrictionsToRingRTC(restrictions);
|
||||
strictAssert(
|
||||
newRestrictions !== CallLinkRestrictions.Unknown,
|
||||
'Invalid call link restrictions value'
|
||||
);
|
||||
|
||||
const result = await RingRTC.updateCallLinkRestrictions(
|
||||
sfuUrl,
|
||||
authCredentialPresentation.serialize(),
|
||||
callLinkRootKey,
|
||||
callLinkAdminKey,
|
||||
newRestrictions
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
const message = `Failed to update call link restrictions: ${result.errorStatusCode}`;
|
||||
log.error(`${logId}: ${message}`);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
log.info(`${logId}: success`);
|
||||
return callLinkStateFromRingRTC(result.value);
|
||||
}
|
||||
|
||||
async readCallLink({
|
||||
callLinkRootKey,
|
||||
}: Readonly<{
|
||||
|
@ -1348,9 +1518,7 @@ export class CallingClass {
|
|||
const callLinkRootKey = CallLinkRootKey.parse(rootKey);
|
||||
const authCredentialPresentation =
|
||||
await getCallLinkAuthCredentialPresentation(callLinkRootKey);
|
||||
const adminPasskey = adminKey
|
||||
? Buffer.from(toAdminKeyBytes(adminKey))
|
||||
: undefined;
|
||||
const adminPasskey = adminKey ? toAdminKeyBytes(adminKey) : undefined;
|
||||
|
||||
// RingRTC reuses the same type GroupCall between Adhoc and Group calls.
|
||||
const groupCall = this.connectCallLinkCall({
|
||||
|
|
|
@ -707,7 +707,7 @@ export type DataInterface = {
|
|||
updateCallLinkState(
|
||||
roomId: string,
|
||||
callLinkState: CallLinkStateType
|
||||
): Promise<void>;
|
||||
): Promise<CallLinkType>;
|
||||
migrateConversationMessages: (
|
||||
obsoleteId: string,
|
||||
currentId: string
|
||||
|
|
|
@ -92,7 +92,7 @@ export async function insertCallLink(callLink: CallLinkType): Promise<void> {
|
|||
export async function updateCallLinkState(
|
||||
roomId: string,
|
||||
callLinkState: CallLinkStateType
|
||||
): Promise<void> {
|
||||
): Promise<CallLinkType> {
|
||||
const { name, restrictions, expiration, revoked } = callLinkState;
|
||||
const db = await getWritableInstance();
|
||||
const restrictionsValue = callLinkRestrictionsSchema.parse(restrictions);
|
||||
|
@ -103,9 +103,12 @@ export async function updateCallLinkState(
|
|||
restrictions = ${restrictionsValue},
|
||||
expiration = ${expiration},
|
||||
revoked = ${revoked ? 1 : 0}
|
||||
WHERE roomId = ${roomId};
|
||||
WHERE roomId = ${roomId}
|
||||
RETURNING *;
|
||||
`;
|
||||
db.prepare(query).run(params);
|
||||
const row = db.prepare(query).get(params);
|
||||
strictAssert(row, 'Expected row to be returned');
|
||||
return callLinkFromRecord(callLinkRecordSchema.parse(row));
|
||||
}
|
||||
|
||||
export async function updateCallLinkAdminKeyByRoomId(
|
||||
|
|
|
@ -115,7 +115,9 @@ function markCallsTabViewed(): ThunkAction<
|
|||
};
|
||||
}
|
||||
|
||||
function addCallHistory(callHistory: CallHistoryDetails): CallHistoryAdd {
|
||||
export function addCallHistory(
|
||||
callHistory: CallHistoryDetails
|
||||
): CallHistoryAdd {
|
||||
return {
|
||||
type: CALL_HISTORY_ADD,
|
||||
payload: callHistory,
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
GroupCallEndReason,
|
||||
type Reaction as CallReaction,
|
||||
} from '@signalapp/ringrtc';
|
||||
import { v4 as generateUuid } from 'uuid';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
import * as Errors from '../../types/errors';
|
||||
import { getIntl, getPlatform } from '../selectors/user';
|
||||
|
@ -31,7 +32,11 @@ import type {
|
|||
PresentedSource,
|
||||
PresentableSource,
|
||||
} from '../../types/Calling';
|
||||
import type { CallLinkStateType, CallLinkType } from '../../types/CallLink';
|
||||
import type {
|
||||
CallLinkRestrictions,
|
||||
CallLinkStateType,
|
||||
CallLinkType,
|
||||
} from '../../types/CallLink';
|
||||
import {
|
||||
CALLING_REACTIONS_LIFETIME,
|
||||
MAX_CALLING_REACTIONS,
|
||||
|
@ -48,6 +53,7 @@ import { requestCameraPermissions } from '../../util/callingPermissions';
|
|||
import {
|
||||
CALL_LINK_DEFAULT_STATE,
|
||||
getRoomIdFromRootKey,
|
||||
isCallLinksCreateEnabled,
|
||||
toAdminKeyBytes,
|
||||
} from '../../util/callLinks';
|
||||
import { sendCallLinkUpdateSync } from '../../util/sendCallLinkUpdateSync';
|
||||
|
@ -82,6 +88,14 @@ import { ButtonVariant } from '../../components/Button';
|
|||
import { getConversationIdForLogging } from '../../util/idForLogging';
|
||||
import dataInterface from '../../sql/Client';
|
||||
import { isAciString } from '../../util/isAciString';
|
||||
import type { CallHistoryDetails } from '../../types/CallDisposition';
|
||||
import {
|
||||
AdhocCallStatus,
|
||||
CallDirection,
|
||||
CallType,
|
||||
} from '../../types/CallDisposition';
|
||||
import type { CallHistoryAdd } from './callHistory';
|
||||
import { addCallHistory } from './callHistory';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -1865,6 +1879,89 @@ function onOutgoingAudioCallInConversation(
|
|||
};
|
||||
}
|
||||
|
||||
function createCallLink(
|
||||
onCreated: (roomId: string) => void
|
||||
): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
CallHistoryAdd | HandleCallLinkUpdateActionType
|
||||
> {
|
||||
return async dispatch => {
|
||||
strictAssert(isCallLinksCreateEnabled(), 'Call links creation is disabled');
|
||||
|
||||
const callLink = await calling.createCallLink();
|
||||
const callHistory: CallHistoryDetails = {
|
||||
callId: generateUuid(),
|
||||
peerId: callLink.roomId,
|
||||
ringerId: null,
|
||||
mode: CallMode.Adhoc,
|
||||
type: CallType.Adhoc,
|
||||
direction: CallDirection.Incoming,
|
||||
timestamp: Date.now(),
|
||||
status: AdhocCallStatus.Pending,
|
||||
};
|
||||
await Promise.all([
|
||||
dataInterface.insertCallLink(callLink),
|
||||
dataInterface.saveCallHistory(callHistory),
|
||||
]);
|
||||
dispatch({
|
||||
type: HANDLE_CALL_LINK_UPDATE,
|
||||
payload: { callLink },
|
||||
});
|
||||
dispatch(addCallHistory(callHistory));
|
||||
// Call after dispatching the action to ensure the call link is in the store
|
||||
onCreated(callLink.roomId);
|
||||
};
|
||||
}
|
||||
|
||||
function updateCallLinkName(
|
||||
roomId: string,
|
||||
name: string
|
||||
): ThunkAction<void, RootStateType, unknown, HandleCallLinkUpdateActionType> {
|
||||
return async dispatch => {
|
||||
const prevCallLink = await dataInterface.getCallLinkByRoomId(roomId);
|
||||
strictAssert(
|
||||
prevCallLink,
|
||||
`updateCallLinkName(${roomId}): call link not found`
|
||||
);
|
||||
const callLinkState = await calling.updateCallLinkName(prevCallLink, name);
|
||||
const callLink = await dataInterface.updateCallLinkState(
|
||||
roomId,
|
||||
callLinkState
|
||||
);
|
||||
dispatch({
|
||||
type: HANDLE_CALL_LINK_UPDATE,
|
||||
payload: { callLink },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function updateCallLinkRestrictions(
|
||||
roomId: string,
|
||||
restrictions: CallLinkRestrictions
|
||||
): ThunkAction<void, RootStateType, unknown, HandleCallLinkUpdateActionType> {
|
||||
return async dispatch => {
|
||||
const prevCallLink = await dataInterface.getCallLinkByRoomId(roomId);
|
||||
strictAssert(
|
||||
prevCallLink,
|
||||
`updateCallLinkRestrictions(${roomId}): call link not found`
|
||||
);
|
||||
const callLinkState = await calling.updateCallLinkRestrictions(
|
||||
prevCallLink,
|
||||
restrictions
|
||||
);
|
||||
const callLink = await dataInterface.updateCallLinkState(
|
||||
roomId,
|
||||
callLinkState
|
||||
);
|
||||
dispatch({
|
||||
type: HANDLE_CALL_LINK_UPDATE,
|
||||
payload: { callLink },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function startCallLinkLobbyByRoomId(
|
||||
roomId: string
|
||||
): StartCallLinkLobbyThunkActionType {
|
||||
|
@ -1977,9 +2074,7 @@ const _startCallLinkLobby = async ({
|
|||
0;
|
||||
|
||||
const { adminKey } = getOwn(state.calling.callLinks, roomId) ?? {};
|
||||
const adminPasskey = adminKey
|
||||
? Buffer.from(toAdminKeyBytes(adminKey))
|
||||
: undefined;
|
||||
const adminPasskey = adminKey ? toAdminKeyBytes(adminKey) : undefined;
|
||||
const callLobbyData = await calling.startCallLinkLobby({
|
||||
callLinkRootKey,
|
||||
adminPasskey,
|
||||
|
@ -2182,6 +2277,7 @@ export const actions = {
|
|||
changeCallView,
|
||||
changeIODevice,
|
||||
closeNeedPermissionScreen,
|
||||
createCallLink,
|
||||
declineCall,
|
||||
denyUser,
|
||||
getPresentingSources,
|
||||
|
@ -2227,6 +2323,8 @@ export const actions = {
|
|||
togglePip,
|
||||
toggleScreenRecordingPermissionsDialog,
|
||||
toggleSettings,
|
||||
updateCallLinkName,
|
||||
updateCallLinkRestrictions,
|
||||
};
|
||||
|
||||
export const useCallingActions = (): BoundActionCreatorsMapObject<
|
||||
|
|
|
@ -45,6 +45,9 @@ import {
|
|||
} from '../selectors/conversations';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { ForwardMessagesModalType } from '../../components/ForwardMessagesModal';
|
||||
import type { CallLinkType } from '../../types/CallLink';
|
||||
import type { LocalizerType } from '../../types/I18N';
|
||||
import { linkCallRoute } from '../../util/signalRoutes';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -88,6 +91,7 @@ type MigrateToGV2PropsType = ReadonlyDeep<{
|
|||
export type GlobalModalsStateType = ReadonlyDeep<{
|
||||
addUserToAnotherGroupModalContactId?: string;
|
||||
aboutContactModalContactId?: string;
|
||||
callLinkEditModalRoomId: string | null;
|
||||
contactModalState?: ContactModalStateType;
|
||||
deleteMessagesProps?: DeleteMessagesPropsType;
|
||||
editHistoryMessages?: EditHistoryMessagesType;
|
||||
|
@ -139,6 +143,7 @@ export const TOGGLE_PROFILE_EDITOR_ERROR =
|
|||
const TOGGLE_SAFETY_NUMBER_MODAL = 'globalModals/TOGGLE_SAFETY_NUMBER_MODAL';
|
||||
const TOGGLE_ADD_USER_TO_ANOTHER_GROUP_MODAL =
|
||||
'globalModals/TOGGLE_ADD_USER_TO_ANOTHER_GROUP_MODAL';
|
||||
const TOGGLE_CALL_LINK_EDIT_MODAL = 'globalModals/TOGGLE_CALL_LINK_EDIT_MODAL';
|
||||
const TOGGLE_ABOUT_MODAL = 'globalModals/TOGGLE_ABOUT_MODAL';
|
||||
const TOGGLE_SIGNAL_CONNECTIONS_MODAL =
|
||||
'globalModals/TOGGLE_SIGNAL_CONNECTIONS_MODAL';
|
||||
|
@ -239,6 +244,11 @@ type ToggleAddUserToAnotherGroupModalActionType = ReadonlyDeep<{
|
|||
payload: string | undefined;
|
||||
}>;
|
||||
|
||||
type ToggleCallLinkEditModalActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_CALL_LINK_EDIT_MODAL;
|
||||
payload: string | null;
|
||||
}>;
|
||||
|
||||
type ToggleAboutContactModalActionType = ReadonlyDeep<{
|
||||
type: typeof TOGGLE_ABOUT_MODAL;
|
||||
payload: string | undefined;
|
||||
|
@ -364,6 +374,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
|
|||
| StartMigrationToGV2ActionType
|
||||
| ToggleAboutContactModalActionType
|
||||
| ToggleAddUserToAnotherGroupModalActionType
|
||||
| ToggleCallLinkEditModalActionType
|
||||
| ToggleConfirmationModalActionType
|
||||
| ToggleDeleteMessagesModalActionType
|
||||
| ToggleForwardMessagesModalActionType
|
||||
|
@ -395,6 +406,7 @@ export const actions = {
|
|||
toggleEditNicknameAndNoteModal,
|
||||
toggleMessageRequestActionsConfirmation,
|
||||
showGV2MigrationDialog,
|
||||
showShareCallLinkViaSignal,
|
||||
showShortcutGuideModal,
|
||||
showStickerPackPreview,
|
||||
showStoriesSettings,
|
||||
|
@ -402,6 +414,7 @@ export const actions = {
|
|||
showWhatsNewModal,
|
||||
toggleAboutContactModal,
|
||||
toggleAddUserToAnotherGroupModal,
|
||||
toggleCallLinkEditModal,
|
||||
toggleConfirmationModal,
|
||||
toggleDeleteMessagesModal,
|
||||
toggleForwardMessagesModal,
|
||||
|
@ -619,6 +632,48 @@ function toggleForwardMessagesModal(
|
|||
};
|
||||
}
|
||||
|
||||
function showShareCallLinkViaSignal(
|
||||
callLink: CallLinkType,
|
||||
i18n: LocalizerType
|
||||
): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
ToggleForwardMessagesModalActionType
|
||||
> {
|
||||
return dispatch => {
|
||||
const url = linkCallRoute
|
||||
.toWebUrl({
|
||||
key: callLink.rootKey,
|
||||
})
|
||||
.toString();
|
||||
dispatch(
|
||||
toggleForwardMessagesModal({
|
||||
type: ForwardMessagesModalType.ShareCallLink,
|
||||
draft: {
|
||||
originalMessageId: null,
|
||||
hasContact: false,
|
||||
isSticker: false,
|
||||
previews: [
|
||||
{
|
||||
title: callLink.name,
|
||||
url,
|
||||
isCallLink: true,
|
||||
},
|
||||
],
|
||||
messageBody: i18n(
|
||||
'icu:ShareCallLinkViaSignal__DraftMessageText',
|
||||
{
|
||||
url,
|
||||
},
|
||||
{ textIsBidiFreeSkipNormalization: true }
|
||||
),
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function toggleNotePreviewModal(
|
||||
payload: NotePreviewModalPropsType | null
|
||||
): ToggleNotePreviewModalActionType {
|
||||
|
@ -656,6 +711,15 @@ function toggleAddUserToAnotherGroupModal(
|
|||
};
|
||||
}
|
||||
|
||||
function toggleCallLinkEditModal(
|
||||
roomId: string | null
|
||||
): ToggleCallLinkEditModalActionType {
|
||||
return {
|
||||
type: TOGGLE_CALL_LINK_EDIT_MODAL,
|
||||
payload: roomId,
|
||||
};
|
||||
}
|
||||
|
||||
function toggleAboutContactModal(
|
||||
contactId?: string
|
||||
): ToggleAboutContactModalActionType {
|
||||
|
@ -871,6 +935,7 @@ function copyOverMessageAttributesIntoForwardMessages(
|
|||
export function getEmptyState(): GlobalModalsStateType {
|
||||
return {
|
||||
hasConfirmationModal: false,
|
||||
callLinkEditModalRoomId: null,
|
||||
editNicknameAndNoteModalProps: null,
|
||||
isProfileEditorVisible: false,
|
||||
isShortcutGuideModalVisible: false,
|
||||
|
@ -984,6 +1049,13 @@ export function reducer(
|
|||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_CALL_LINK_EDIT_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
callLinkEditModalRoomId: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === TOGGLE_DELETE_MESSAGES_MODAL) {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -22,6 +22,11 @@ export const isShowingAnyModal = createSelector(
|
|||
})
|
||||
);
|
||||
|
||||
export const getCallLinkEditModalRoomId = createSelector(
|
||||
getGlobalModalsState,
|
||||
({ callLinkEditModalRoomId }) => callLinkEditModalRoomId
|
||||
);
|
||||
|
||||
export const getContactModalState = createSelector(
|
||||
getGlobalModalsState,
|
||||
({ contactModalState }) => contactModalState
|
||||
|
|
|
@ -10,8 +10,6 @@ import { useGlobalModalActions } from '../ducks/globalModals';
|
|||
import { useCallingActions } from '../ducks/calling';
|
||||
import * as log from '../../logging/log';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { linkCallRoute } from '../../util/signalRoutes';
|
||||
import { ForwardMessagesModalType } from '../../components/ForwardMessagesModal';
|
||||
|
||||
export type SmartCallLinkDetailsProps = Readonly<{
|
||||
roomId: string;
|
||||
|
@ -25,40 +23,14 @@ export const SmartCallLinkDetails = memo(function SmartCallLinkDetails({
|
|||
const i18n = useSelector(getIntl);
|
||||
const callLinkSelector = useSelector(getCallLinkSelector);
|
||||
const { startCallLinkLobby } = useCallingActions();
|
||||
const { toggleForwardMessagesModal } = useGlobalModalActions();
|
||||
const { showShareCallLinkViaSignal } = useGlobalModalActions();
|
||||
|
||||
const callLink = callLinkSelector(roomId);
|
||||
|
||||
const handleShareCallLinkViaSignal = useCallback(() => {
|
||||
strictAssert(callLink != null, 'callLink not found');
|
||||
const url = linkCallRoute
|
||||
.toWebUrl({
|
||||
key: callLink.rootKey,
|
||||
})
|
||||
.toString();
|
||||
toggleForwardMessagesModal({
|
||||
type: ForwardMessagesModalType.ShareCallLink,
|
||||
draft: {
|
||||
originalMessageId: null,
|
||||
hasContact: false,
|
||||
isSticker: false,
|
||||
previews: [
|
||||
{
|
||||
title: callLink.name,
|
||||
url,
|
||||
isCallLink: true,
|
||||
},
|
||||
],
|
||||
messageBody: i18n(
|
||||
'icu:ShareCallLinkViaSignal__DraftMessageText',
|
||||
{
|
||||
url,
|
||||
},
|
||||
{ textIsBidiFreeSkipNormalization: true }
|
||||
),
|
||||
},
|
||||
});
|
||||
}, [callLink, i18n, toggleForwardMessagesModal]);
|
||||
showShareCallLinkViaSignal(callLink, i18n);
|
||||
}, [callLink, i18n, showShareCallLinkViaSignal]);
|
||||
|
||||
const handleStartCallLinkLobby = useCallback(() => {
|
||||
strictAssert(callLink != null, 'callLink not found');
|
||||
|
|
101
ts/state/smart/CallLinkEditModal.tsx
Normal file
101
ts/state/smart/CallLinkEditModal.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CallLinkEditModal } from '../../components/CallLinkEditModal';
|
||||
import { useCallingActions } from '../ducks/calling';
|
||||
import { getCallLinkSelector } from '../selectors/calling';
|
||||
import * as log from '../../logging/log';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import type { CallLinkRestrictions } from '../../types/CallLink';
|
||||
import { getCallLinkEditModalRoomId } from '../selectors/globalModals';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { linkCallRoute } from '../../util/signalRoutes';
|
||||
import { copyCallLink } from '../../util/copyLinksWithToast';
|
||||
import { drop } from '../../util/drop';
|
||||
import { isCallLinksCreateEnabled } from '../../util/callLinks';
|
||||
|
||||
export const SmartCallLinkEditModal = memo(
|
||||
function SmartCallLinkEditModal(): JSX.Element | null {
|
||||
strictAssert(isCallLinksCreateEnabled(), 'Call links creation is disabled');
|
||||
|
||||
const roomId = useSelector(getCallLinkEditModalRoomId);
|
||||
strictAssert(roomId, 'Expected roomId to be set');
|
||||
|
||||
const i18n = useSelector(getIntl);
|
||||
const callLinkSelector = useSelector(getCallLinkSelector);
|
||||
|
||||
const {
|
||||
updateCallLinkName,
|
||||
updateCallLinkRestrictions,
|
||||
startCallLinkLobby,
|
||||
} = useCallingActions();
|
||||
const { toggleCallLinkEditModal, showShareCallLinkViaSignal } =
|
||||
useGlobalModalActions();
|
||||
|
||||
const callLink = useMemo(() => {
|
||||
return callLinkSelector(roomId);
|
||||
}, [callLinkSelector, roomId]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
toggleCallLinkEditModal(null);
|
||||
}, [toggleCallLinkEditModal]);
|
||||
|
||||
const handleCopyCallLink = useCallback(() => {
|
||||
strictAssert(callLink != null, 'callLink not found');
|
||||
const callLinkWebUrl = linkCallRoute
|
||||
.toWebUrl({
|
||||
key: callLink?.rootKey,
|
||||
})
|
||||
.toString();
|
||||
drop(copyCallLink(callLinkWebUrl));
|
||||
}, [callLink]);
|
||||
|
||||
const handleUpdateCallLinkName = useCallback(
|
||||
(newName: string) => {
|
||||
updateCallLinkName(roomId, newName);
|
||||
},
|
||||
[roomId, updateCallLinkName]
|
||||
);
|
||||
|
||||
const handleUpdateCallLinkRestrictions = useCallback(
|
||||
(newRestrictions: CallLinkRestrictions) => {
|
||||
updateCallLinkRestrictions(roomId, newRestrictions);
|
||||
},
|
||||
[roomId, updateCallLinkRestrictions]
|
||||
);
|
||||
|
||||
const handleShareCallLinkViaSignal = useCallback(() => {
|
||||
strictAssert(callLink != null, 'callLink not found');
|
||||
showShareCallLinkViaSignal(callLink, i18n);
|
||||
}, [callLink, i18n, showShareCallLinkViaSignal]);
|
||||
|
||||
const handleStartCallLinkLobby = useCallback(() => {
|
||||
strictAssert(callLink != null, 'callLink not found');
|
||||
startCallLinkLobby({ rootKey: callLink.rootKey });
|
||||
toggleCallLinkEditModal(null);
|
||||
}, [callLink, startCallLinkLobby, toggleCallLinkEditModal]);
|
||||
|
||||
if (!callLink) {
|
||||
log.error(
|
||||
'SmartCallLinkEditModal: No call link found for roomId',
|
||||
roomId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CallLinkEditModal
|
||||
i18n={i18n}
|
||||
callLink={callLink}
|
||||
onClose={handleClose}
|
||||
onCopyCallLink={handleCopyCallLink}
|
||||
onUpdateCallLinkName={handleUpdateCallLinkName}
|
||||
onUpdateCallLinkRestrictions={handleUpdateCallLinkRestrictions}
|
||||
onShareCallLinkViaSignal={handleShareCallLinkViaSignal}
|
||||
onStartCallLinkLobby={handleStartCallLinkLobby}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, useCallback, useEffect } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useItemsActions } from '../ducks/items';
|
||||
import {
|
||||
|
@ -40,6 +40,8 @@ import { getOtherTabsUnreadStats } from '../selectors/nav';
|
|||
import { SmartCallLinkDetails } from './CallLinkDetails';
|
||||
import type { CallLinkType } from '../../types/CallLink';
|
||||
import { filterCallLinks } from '../../util/filterCallLinks';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { isCallLinksCreateEnabled } from '../../util/callLinks';
|
||||
|
||||
function getCallHistoryFilter({
|
||||
allCallLinks,
|
||||
|
@ -151,7 +153,12 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
const hasFailedStorySends = useSelector(getHasAnyFailedStorySends);
|
||||
const otherTabsUnreadStats = useSelector(getOtherTabsUnreadStats);
|
||||
|
||||
const canCreateCallLinks = useMemo(() => {
|
||||
return isCallLinksCreateEnabled();
|
||||
}, []);
|
||||
|
||||
const {
|
||||
createCallLink,
|
||||
hangUpActiveCall,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
|
@ -164,6 +171,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
markCallHistoryRead,
|
||||
markCallsTabViewed,
|
||||
} = useCallHistoryActions();
|
||||
const { toggleCallLinkEditModal } = useGlobalModalActions();
|
||||
|
||||
const getCallHistoryGroupsCount = useCallback(
|
||||
async (options: CallHistoryFilterOptions) => {
|
||||
|
@ -207,6 +215,12 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
[allCallLinks, allConversations, regionCode]
|
||||
);
|
||||
|
||||
const handleCreateCallLink = useCallback(() => {
|
||||
createCallLink(roomId => {
|
||||
toggleCallLinkEditModal(roomId);
|
||||
});
|
||||
}, [createCallLink, toggleCallLinkEditModal]);
|
||||
|
||||
useEffect(() => {
|
||||
markCallsTabViewed();
|
||||
}, [markCallsTabViewed]);
|
||||
|
@ -223,6 +237,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
getCall={getCall}
|
||||
getCallLink={getCallLink}
|
||||
callHistoryEdition={callHistoryEdition}
|
||||
canCreateCallLinks={canCreateCallLinks}
|
||||
hangUpActiveCall={hangUpActiveCall}
|
||||
hasFailedStorySends={hasFailedStorySends}
|
||||
hasPendingUpdate={hasPendingUpdate}
|
||||
|
@ -231,6 +246,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
onClearCallHistory={clearCallHistory}
|
||||
onMarkCallHistoryRead={markCallHistoryRead}
|
||||
onToggleNavTabsCollapse={toggleNavTabsCollapse}
|
||||
onCreateCallLink={handleCreateCallLink}
|
||||
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
||||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||
peekNotConnectedGroupCall={peekNotConnectedGroupCall}
|
||||
|
|
|
@ -26,6 +26,11 @@ import { SmartMessageRequestActionsConfirmation } from './MessageRequestActionsC
|
|||
import { getGlobalModalsState } from '../selectors/globalModals';
|
||||
import { SmartEditNicknameAndNoteModal } from './EditNicknameAndNoteModal';
|
||||
import { SmartNotePreviewModal } from './NotePreviewModal';
|
||||
import { SmartCallLinkEditModal } from './CallLinkEditModal';
|
||||
|
||||
function renderCallLinkEditModal(): JSX.Element {
|
||||
return <SmartCallLinkEditModal />;
|
||||
}
|
||||
|
||||
function renderEditHistoryMessagesModal(): JSX.Element {
|
||||
return <SmartEditHistoryMessagesModal />;
|
||||
|
@ -90,6 +95,7 @@ export const SmartGlobalModalContainer = memo(
|
|||
const {
|
||||
aboutContactModalContactId,
|
||||
addUserToAnotherGroupModalContactId,
|
||||
callLinkEditModalRoomId,
|
||||
contactModalState,
|
||||
deleteMessagesProps,
|
||||
editHistoryMessages,
|
||||
|
@ -168,6 +174,7 @@ export const SmartGlobalModalContainer = memo(
|
|||
addUserToAnotherGroupModalContactId={
|
||||
addUserToAnotherGroupModalContactId
|
||||
}
|
||||
callLinkEditModalRoomId={callLinkEditModalRoomId}
|
||||
contactModalState={contactModalState}
|
||||
editHistoryMessages={editHistoryMessages}
|
||||
editNicknameAndNoteModalProps={editNicknameAndNoteModalProps}
|
||||
|
@ -190,6 +197,7 @@ export const SmartGlobalModalContainer = memo(
|
|||
isWhatsNewVisible={isWhatsNewVisible}
|
||||
renderAboutContactModal={renderAboutContactModal}
|
||||
renderAddUserToAnotherGroup={renderAddUserToAnotherGroup}
|
||||
renderCallLinkEditModal={renderCallLinkEditModal}
|
||||
renderContactModal={renderContactModal}
|
||||
renderEditHistoryMessagesModal={renderEditHistoryMessagesModal}
|
||||
renderEditNicknameAndNoteModal={renderEditNicknameAndNoteModal}
|
||||
|
|
|
@ -645,6 +645,7 @@ export class Bootstrap {
|
|||
storageProfile: 'mock',
|
||||
serverUrl: url,
|
||||
storageUrl: url,
|
||||
sfuUrl: url,
|
||||
cdn: {
|
||||
'0': url,
|
||||
'2': url,
|
||||
|
|
64
ts/test-mock/calling/callLinkAdmin_test.ts
Normal file
64
ts/test-mock/calling/callLinkAdmin_test.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { expect } from 'playwright/test';
|
||||
import * as durations from '../../util/durations';
|
||||
import type { App } from '../playwright';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
|
||||
describe('calling/callLinkAdmin', function (this: Mocha.Suite) {
|
||||
this.timeout(durations.MINUTE);
|
||||
|
||||
let bootstrap: Bootstrap;
|
||||
let app: App;
|
||||
|
||||
beforeEach(async () => {
|
||||
bootstrap = new Bootstrap();
|
||||
await bootstrap.init();
|
||||
app = await bootstrap.link();
|
||||
});
|
||||
|
||||
afterEach(async function (this: Mocha.Context) {
|
||||
if (!bootstrap) {
|
||||
return;
|
||||
}
|
||||
|
||||
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||
await app.close();
|
||||
await bootstrap.teardown();
|
||||
});
|
||||
|
||||
it('can create and edit a call link', async () => {
|
||||
const window = await app.getWindow();
|
||||
|
||||
await window.locator('[data-testid="NavTabsItem--Calls"]').click();
|
||||
|
||||
await window
|
||||
.locator('.NavSidebar__HeaderTitle')
|
||||
.getByText('Calls')
|
||||
.waitFor();
|
||||
|
||||
await window
|
||||
.locator('.CallsList__ItemTile')
|
||||
.getByText('Create a Call Link')
|
||||
.click();
|
||||
|
||||
const callLinkItem = window.locator('.CallsList__Item[data-type="Adhoc"]');
|
||||
|
||||
const modal = window.locator('.CallLinkEditModal');
|
||||
await modal.waitFor();
|
||||
|
||||
const row = modal.locator('.CallLinkEditModal__ApproveAllMembers__Row');
|
||||
|
||||
await expect(row).toHaveAttribute('data-restrictions', '0');
|
||||
|
||||
const select = modal.locator('select');
|
||||
await select.selectOption({ label: 'On' });
|
||||
await expect(row).toHaveAttribute('data-restrictions', '1');
|
||||
|
||||
const nameInput = modal.locator('.CallLinkEditModal__Input--Name__input');
|
||||
await nameInput.fill('New Name');
|
||||
await nameInput.blur();
|
||||
|
||||
await expect(callLinkItem).toContainText('New Name');
|
||||
});
|
||||
});
|
|
@ -570,6 +570,7 @@ const URL_CALLS = {
|
|||
backupMedia: 'v1/archives/media',
|
||||
backupMediaBatch: 'v1/archives/media/batch',
|
||||
backupMediaDelete: 'v1/archives/media/delete',
|
||||
callLinkCreateAuth: 'v1/call-link/create-auth',
|
||||
registration: 'v1/registration',
|
||||
registerCapabilities: 'v1/devices/capabilities',
|
||||
reportMessage: 'v1/messages/report',
|
||||
|
@ -666,7 +667,7 @@ type AjaxOptionsType = {
|
|||
basicAuth?: string;
|
||||
call: keyof typeof URL_CALLS;
|
||||
contentType?: string;
|
||||
data?: Uint8Array | Buffer | Uint8Array | string;
|
||||
data?: Buffer | Uint8Array | string;
|
||||
disableSessionResumption?: boolean;
|
||||
headers?: HeaderListType;
|
||||
host?: string;
|
||||
|
@ -1148,6 +1149,14 @@ export type GetBackupInfoResponseType = z.infer<
|
|||
typeof getBackupInfoResponseSchema
|
||||
>;
|
||||
|
||||
export type CallLinkCreateAuthResponseType = Readonly<{
|
||||
credential: string;
|
||||
}>;
|
||||
|
||||
export const callLinkCreateAuthResponseSchema = z.object({
|
||||
credential: z.string(),
|
||||
}) satisfies z.ZodSchema<CallLinkCreateAuthResponseType>;
|
||||
|
||||
const StickerPackUploadAttributesSchema = z.object({
|
||||
acl: z.string(),
|
||||
algorithm: z.string(),
|
||||
|
@ -1384,6 +1393,9 @@ export type WebAPIType = {
|
|||
options: BackupListMediaOptionsType
|
||||
) => Promise<BackupListMediaResponseType>;
|
||||
backupDeleteMedia: (options: BackupDeleteMediaOptionsType) => Promise<void>;
|
||||
callLinkCreateAuth: (
|
||||
requestBase64: string
|
||||
) => Promise<CallLinkCreateAuthResponseType>;
|
||||
setPhoneNumberDiscoverability: (newValue: boolean) => Promise<void>;
|
||||
updateDeviceName: (deviceName: string) => Promise<void>;
|
||||
uploadAvatar: (
|
||||
|
@ -1666,6 +1678,7 @@ export function initialize({
|
|||
checkAccountExistence,
|
||||
checkSockets,
|
||||
createAccount,
|
||||
callLinkCreateAuth,
|
||||
createFetchForAttachmentUpload,
|
||||
confirmUsername,
|
||||
createGroup,
|
||||
|
@ -2959,6 +2972,18 @@ export function initialize({
|
|||
return backupListMediaResponseSchema.parse(res);
|
||||
}
|
||||
|
||||
async function callLinkCreateAuth(
|
||||
requestBase64: string
|
||||
): Promise<CallLinkCreateAuthResponseType> {
|
||||
const response = await _ajax({
|
||||
call: 'callLinkCreateAuth',
|
||||
httpType: 'POST',
|
||||
responseType: 'json',
|
||||
jsonData: { createCallLinkCredentialRequest: requestBase64 },
|
||||
});
|
||||
return callLinkCreateAuthResponseSchema.parse(response);
|
||||
}
|
||||
|
||||
async function setPhoneNumberDiscoverability(newValue: boolean) {
|
||||
await _ajax({
|
||||
call: 'phoneNumberDiscoverability',
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import { z } from 'zod';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { safeParseInteger } from '../util/numbers';
|
||||
|
||||
export enum CallLinkUpdateSyncType {
|
||||
Update = 'Update',
|
||||
|
@ -28,9 +29,9 @@ export enum CallLinkRestrictions {
|
|||
export const callLinkRestrictionsSchema = z.nativeEnum(CallLinkRestrictions);
|
||||
|
||||
export function toCallLinkRestrictions(
|
||||
restrictions: number
|
||||
restrictions: number | string
|
||||
): CallLinkRestrictions {
|
||||
return callLinkRestrictionsSchema.parse(restrictions);
|
||||
return callLinkRestrictionsSchema.parse(safeParseInteger(restrictions));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { CallLinkRootKey } from '@signalapp/ringrtc';
|
||||
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 * as RemoteConfig from '../RemoteConfig';
|
||||
import type { CallLinkAuthCredentialPresentation } from './zkgroup';
|
||||
import {
|
||||
CallLinkAuthCredential,
|
||||
|
@ -15,6 +21,7 @@ import type {
|
|||
CallLinkConversationType,
|
||||
CallLinkType,
|
||||
CallLinkRecord,
|
||||
CallLinkStateType,
|
||||
} from '../types/CallLink';
|
||||
import {
|
||||
callLinkRecordSchema,
|
||||
|
@ -22,6 +29,7 @@ import {
|
|||
toCallLinkRestrictions,
|
||||
} from '../types/CallLink';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { isTestOrMockEnvironment } from '../environment';
|
||||
|
||||
export const CALL_LINK_DEFAULT_STATE = {
|
||||
name: '',
|
||||
|
@ -30,6 +38,13 @@ export const CALL_LINK_DEFAULT_STATE = {
|
|||
expiration: null,
|
||||
};
|
||||
|
||||
export function isCallLinksCreateEnabled(): boolean {
|
||||
if (isTestOrMockEnvironment()) {
|
||||
return true;
|
||||
}
|
||||
return RemoteConfig.getValue('desktop.calling.adhoc.create') === 'TRUE';
|
||||
}
|
||||
|
||||
export function getRoomIdFromRootKey(rootKey: CallLinkRootKey): string {
|
||||
return rootKey.deriveRoomId().toString('hex');
|
||||
}
|
||||
|
@ -112,14 +127,39 @@ export function fromRootKeyBytes(rootKey: Uint8Array): string {
|
|||
return CallLinkRootKey.fromBytes(rootKey as Buffer).toString();
|
||||
}
|
||||
|
||||
export function toAdminKeyBytes(adminKey: string): Uint8Array {
|
||||
return Bytes.fromBase64(adminKey);
|
||||
export function toAdminKeyBytes(adminKey: string): Buffer {
|
||||
return Buffer.from(adminKey, 'base64');
|
||||
}
|
||||
|
||||
export function fromAdminKeyBytes(adminKey: Uint8Array): string {
|
||||
return Bytes.toBase64(adminKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* RingRTC conversions
|
||||
*/
|
||||
|
||||
export function callLinkStateFromRingRTC(
|
||||
state: RingRTCCallLinkState
|
||||
): CallLinkStateType {
|
||||
return {
|
||||
name: state.name,
|
||||
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
|
||||
*/
|
||||
|
|
59
ts/util/numbers.ts
Normal file
59
ts/util/numbers.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { strictAssert } from './assert';
|
||||
|
||||
export function safeParseNumber(value: number | string): number | null {
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
strictAssert(typeof value === 'string', 'Expected string or number');
|
||||
const trimmed = value.trim();
|
||||
if (trimmed === '') {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return null;
|
||||
}
|
||||
if (parsed < Number.MIN_SAFE_INTEGER || parsed > Number.MAX_SAFE_INTEGER) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function safeParseInteger(
|
||||
value: number | string,
|
||||
trunc = false
|
||||
): number | null {
|
||||
const parsed = safeParseNumber(value);
|
||||
if (parsed == null) {
|
||||
return null;
|
||||
}
|
||||
if (trunc) {
|
||||
return Math.trunc(parsed);
|
||||
}
|
||||
if (!Number.isInteger(parsed)) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function safeParseBigint(
|
||||
value: bigint | number | string
|
||||
): bigint | null {
|
||||
if (typeof value === 'bigint') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isInteger(value)) {
|
||||
return null;
|
||||
}
|
||||
return BigInt(value);
|
||||
}
|
||||
strictAssert(typeof value === 'string', 'Expected string, number, or bigint');
|
||||
const trimmed = value.trim();
|
||||
if (trimmed === '') {
|
||||
return null;
|
||||
}
|
||||
return BigInt(value);
|
||||
}
|
|
@ -37,3 +37,7 @@ export function urlPathFromComponents(
|
|||
): string {
|
||||
return `/${components.filter(Boolean).map(encodeURIComponent).join('/')}`;
|
||||
}
|
||||
|
||||
export function formatUrlWithoutProtocol(url: Readonly<URL>): string {
|
||||
return `${url.hostname}${url.pathname}${url.search}${url.hash}`;
|
||||
}
|
||||
|
|
49
yarn.lock
49
yarn.lock
|
@ -4011,10 +4011,10 @@
|
|||
type-fest "^3.5.0"
|
||||
uuid "^8.3.0"
|
||||
|
||||
"@signalapp/mock-server@6.5.0":
|
||||
version "6.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-6.5.0.tgz#0fa420ff2d7386770b3c8dfe6f57be425816a130"
|
||||
integrity sha512-QuEYX9EFFaPIvQzGlHkgfrnpnhs+Q8jOwk2UuE+5txJNXezrQnq1nRFChG+M/XAv0aSbc7thiq8iBxwpN2F2EA==
|
||||
"@signalapp/mock-server@6.6.0":
|
||||
version "6.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/mock-server/-/mock-server-6.6.0.tgz#9abce9c7e634ef4e3a316cdf165965500010bd92"
|
||||
integrity sha512-zeLz9YikLaCQfWgSy2XDeEMdLWUTpyGOteSucD1BLcmv54J2eysk7ppDJns3H13opmF4Qj1Xmy5VftG3V3QKow==
|
||||
dependencies:
|
||||
"@signalapp/libsignal-client" "^0.45.0"
|
||||
debug "^4.3.2"
|
||||
|
@ -7617,7 +7617,7 @@ caniuse-lite@^1.0.30001349, caniuse-lite@^1.0.30001541:
|
|||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001541.tgz"
|
||||
integrity sha512-bLOsqxDgTqUBkzxbNlSBt8annkDpQB9NdzdTbO2ooJ+eC/IQcvDspDc058g84ejCelF7vHUx57KIOjEecOHXaw==
|
||||
|
||||
canvas@^2.6.1, "canvas@https://registry.yarnpkg.com/nop/-/nop-1.0.0.tgz":
|
||||
canvas@^2.6.1, "canvas@https://registry.yarnpkg.com/nop/-/nop-1.0.0.tgz", dmg-license@^1.0.11, "dmg-license@https://registry.yarnpkg.com/nop/-/nop-1.0.0.tgz", jsdom@^15.2.1, "jsdom@https://registry.yarnpkg.com/nop/-/nop-1.0.0.tgz":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/nop/-/nop-1.0.0.tgz#cb46cf7e01574aa6390858149f66897afe53c9ca"
|
||||
|
||||
|
@ -9028,10 +9028,6 @@ dmg-builder@24.6.3:
|
|||
optionalDependencies:
|
||||
dmg-license "^1.0.11"
|
||||
|
||||
dmg-license@^1.0.11, "dmg-license@https://registry.yarnpkg.com/nop/-/nop-1.0.0.tgz":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/nop/-/nop-1.0.0.tgz#cb46cf7e01574aa6390858149f66897afe53c9ca"
|
||||
|
||||
dns-equal@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
|
||||
|
@ -13541,10 +13537,6 @@ jsdoc@^4.0.0:
|
|||
strip-json-comments "^3.1.0"
|
||||
underscore "~1.13.2"
|
||||
|
||||
jsdom@^15.2.1, "jsdom@https://registry.yarnpkg.com/nop/-/nop-1.0.0.tgz":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/nop/-/nop-1.0.0.tgz#cb46cf7e01574aa6390858149f66897afe53c9ca"
|
||||
|
||||
jsesc@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
|
||||
|
@ -18727,7 +18719,7 @@ string-length@^5.0.1:
|
|||
char-regex "^2.0.0"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
|
@ -18771,15 +18763,6 @@ string-width@^4.1.0, string-width@^4.2.0:
|
|||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^5.0.1, string-width@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
|
||||
|
@ -18860,7 +18843,7 @@ string_decoder@~1.1.1:
|
|||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
|
@ -18895,13 +18878,6 @@ strip-ansi@^6.0.0:
|
|||
dependencies:
|
||||
ansi-regex "^5.0.0"
|
||||
|
||||
strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.1:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||
|
@ -20553,7 +20529,7 @@ workerpool@6.2.1:
|
|||
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
|
||||
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
@ -20588,15 +20564,6 @@ wrap-ansi@^6.2.0:
|
|||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
|
Loading…
Reference in a new issue