Init create/admin call links flow

This commit is contained in:
Jamie Kyle 2024-06-10 08:23:43 -07:00 committed by GitHub
parent 53b8f5f152
commit f19f0fb47d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1256 additions and 149 deletions

View file

@ -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,

View file

@ -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<

View file

@ -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,

View file

@ -22,6 +22,11 @@ export const isShowingAnyModal = createSelector(
})
);
export const getCallLinkEditModalRoomId = createSelector(
getGlobalModalsState,
({ callLinkEditModalRoomId }) => callLinkEditModalRoomId
);
export const getContactModalState = createSelector(
getGlobalModalsState,
({ contactModalState }) => contactModalState

View file

@ -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');

View 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}
/>
);
}
);

View file

@ -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}

View file

@ -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}