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