Update call link edit/add name modals

This commit is contained in:
Jamie Kyle 2024-06-25 11:56:28 -07:00 committed by GitHub
parent ba77ef7563
commit b691e24d5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 526 additions and 232 deletions

View file

@ -7208,14 +7208,14 @@
"messageformat": "Call link details", "messageformat": "Call link details",
"description": "Call Link Edit Modal > Title" "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": { "icu:CallLinkEditModal__JoinButtonLabel": {
"messageformat": "Join", "messageformat": "Join",
"description": "Call Link Edit Modal > Join Button > Label" "description": "Call Link Edit Modal > Join Button > Label"
}, },
"icu:CallLinkEditModal__AddCallNameLabel": {
"messageformat": "Add call name",
"description": "Call Link Edit Modal > Add Call Name Button > Label"
},
"icu:CallLinkEditModal__InputLabel--ApproveAllMembers": { "icu:CallLinkEditModal__InputLabel--ApproveAllMembers": {
"messageformat": "Approve all members", "messageformat": "Approve all members",
"description": "Call Link Edit Modal > Approve All Members Checkbox > Label" "description": "Call Link Edit Modal > Approve All Members Checkbox > Label"
@ -7228,6 +7228,14 @@
"messageformat": "On", "messageformat": "On",
"description": "Call Link Edit Modal > Approve All Members Checkbox > Option > On" "description": "Call Link Edit Modal > Approve All Members Checkbox > Option > On"
}, },
"icu:CallLinkAddNameModal__Title": {
"messageformat": "Add call name",
"description": "Call Link Add Name Modal > Title"
},
"icu:CallLinkAddNameModal__NameLabel": {
"messageformat": "Call name",
"description": "Call Link Add Name Modal > Name Input > Label"
},
"icu:TypingBubble__avatar--overflow-count": { "icu:TypingBubble__avatar--overflow-count": {
"messageformat": "{count, plural, one {# other is} other {# others are}} typing.", "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." "description": "Group chat multiple person typing indicator when space isn't available to show every avatar, this is the count of avatars hidden."

View file

@ -0,0 +1,16 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.CallLinkAddNameModal__Row {
display: flex;
gap: 12px;
}
.CallLinkAddNameModal__SrOnly {
@include sr-only;
}
// Overriding the default styles for the input container
.CallLinkAddNameModal__Input__container.Input__container {
flex: 1;
}

View file

@ -1,6 +1,12 @@
// Copyright 2024 Signal Messenger, LLC // Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// Overriding default style
.module-Modal__body.CallLinkEditModal__body {
padding-inline: 12px 3px;
scrollbar-gutter: stable;
}
.CallLinkEditModal__SrOnly { .CallLinkEditModal__SrOnly {
@include sr-only; @include sr-only;
} }
@ -13,67 +19,80 @@
} }
.CallLinkEditModal__Header__Details { .CallLinkEditModal__Header__Details {
flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 0;
min-width: 0; // fix overflow issue min-width: 0; // fix overflow issue
} }
// Overriding default style .CallLinkEditModal__Header__Title {
.Input__container.CallLinkEditModal__Input--Name__container { @include font-body-1-bold;
margin: 0;
} }
.CallLinkEditModal__CallLinkAndJoinButton { .CallLinkEditModal__Header__CallLinkButton {
@include button-reset;
@include font-subtitle;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
.CallLinkEditModal__Header__CallLinkButton__Text {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.CallLinkEditModal__Header__Actions {
display: flex; display: flex;
gap: 14px; gap: 14px;
align-items: center; 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 { .CallLinkEditModal__JoinButton {
@include font-body-1-bold; @include font-body-1-bold;
} }
.CallLinkEditModal__ApproveAllMembers__Row { .CallLinkEditModal__Row {
display: flex; display: flex;
padding: 12px;
align-items: center; align-items: center;
margin-bottom: 18px; gap: 16px;
} }
.CallLinkEditModal__ApproveAllMembers__Label { .CallLinkEditModal__RowButton {
flex: 1;
}
.CallLinkEditModal__ActionButton {
@include button-reset; @include button-reset;
width: 100%;
padding-block: 1px;
.CallLinkEditModal__Row {
border-radius: 8px;
}
&:hover,
&:focus {
.CallLinkEditModal__Row {
@include light-theme {
background: $color-gray-02;
}
@include dark-theme {
background: $color-gray-75;
}
}
}
}
.CallLinkEditModal__Row--Button {
@include font-body-2; @include font-body-2;
display: flex; display: flex;
gap: 8px; gap: 8px;
align-items: center; align-items: center;
padding-block: 8px;
width: 100%; width: 100%;
@include light-theme { @include light-theme {
@ -84,20 +103,17 @@
} }
} }
.CallLinkEditModal__ActionButton__Icon { .CallLinkEditModal__RowLabel {
@include font-body-1;
flex: 1;
}
.CallLinkEditModal__RowIcon {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 36px; width: 20px;
height: 36px; height: 20px;
border-radius: 9999px;
@include light-theme {
background: $color-gray-05;
}
@include dark-theme {
background: $color-gray-65;
}
&::after { &::after {
content: ''; content: '';
@ -110,30 +126,42 @@
} }
} }
.CallLinkEditModal__ActionButton__Icon--Copy { @mixin CallLinkEditModal__RowIcon($iconPath) {
&::after { &::after {
@include light-theme { @include light-theme {
@include color-svg('../images/icons/v3/copy/copy.svg', $color-gray-75); @include color-svg($iconPath, $color-gray-75);
} }
@include dark-theme { @include dark-theme {
@include color-svg('../images/icons/v3/copy/copy.svg', $color-gray-15); @include color-svg($iconPath, $color-gray-15);
} }
} }
} }
.CallLinkEditModal__ActionButton__Icon--Share { .CallLinkEditModal__RowIcon--Edit {
&::after { @include CallLinkEditModal__RowIcon('../images/icons/v3/edit/edit.svg');
@include light-theme { }
@include color-svg(
'../images/icons/v3/forward/forward.svg', .CallLinkEditModal__RowIcon--Approve {
$color-gray-75 @include CallLinkEditModal__RowIcon(
); '../images/icons/v3/person/person-check-compact.svg'
} );
@include dark-theme { }
@include color-svg(
'../images/icons/v3/forward/forward.svg', .CallLinkEditModal__RowIcon--Copy {
$color-gray-15 @include CallLinkEditModal__RowIcon('../images/icons/v3/copy/copy.svg');
); }
}
} .CallLinkEditModal__RowIcon--Share {
@include CallLinkEditModal__RowIcon('../images/icons/v3/forward/forward.svg');
}
.CallLinkEditModal__Hr {
border: none;
height: 1px;
background: $color-black-alpha-12;
}
// Overriding default style
.CallLinkEditModal__RowSelect.module-select select {
min-width: 0;
} }

View file

@ -50,6 +50,7 @@
@import './components/CallingScreenSharingController.scss'; @import './components/CallingScreenSharingController.scss';
@import './components/CallingSelectPresentingSourcesModal.scss'; @import './components/CallingSelectPresentingSourcesModal.scss';
@import './components/CallingToast.scss'; @import './components/CallingToast.scss';
@import './components/CallLinkAddNameModal.scss';
@import './components/CallLinkDetails.scss'; @import './components/CallLinkDetails.scss';
@import './components/CallLinkEditModal.scss'; @import './components/CallLinkEditModal.scss';
@import './components/CallingRaisedHandsList.scss'; @import './components/CallingRaisedHandsList.scss';

View file

@ -0,0 +1,28 @@
// 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 { CallLinkAddNameModalProps } from './CallLinkAddNameModal';
import { CallLinkAddNameModal } from './CallLinkAddNameModal';
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/CallLinkAddNameModal',
component: CallLinkAddNameModal,
args: {
i18n,
callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY,
onClose: action('onClose'),
onUpdateCallLinkName: action('onUpdateCallLinkName'),
},
} satisfies ComponentMeta<CallLinkAddNameModalProps>;
export function Basic(args: CallLinkAddNameModalProps): JSX.Element {
return <CallLinkAddNameModal {...args} />;
}

View file

@ -0,0 +1,98 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useState } from 'react';
import { v4 as generateUuid } from 'uuid';
import { Modal } from './Modal';
import type { LocalizerType } from '../types/I18N';
import { Button, ButtonVariant } from './Button';
import { Avatar, AvatarSize } from './Avatar';
import { Input } from './Input';
import type { CallLinkType } from '../types/CallLink';
export type CallLinkAddNameModalProps = Readonly<{
i18n: LocalizerType;
callLink: CallLinkType;
onClose: () => void;
onUpdateCallLinkName: (name: string) => void;
}>;
export function CallLinkAddNameModal({
i18n,
callLink,
onClose,
onUpdateCallLinkName,
}: CallLinkAddNameModalProps): JSX.Element {
const [formId] = useState(() => generateUuid());
const [nameId] = useState(() => generateUuid());
const [nameInput, setNameInput] = useState(callLink.name);
const handleNameInputChange = useCallback((nextNameInput: string) => {
setNameInput(nextNameInput);
}, []);
const handleSubmit = useCallback(() => {
const nameValue = nameInput.trim();
if (nameValue === callLink.name) {
return;
}
onUpdateCallLinkName(nameValue);
onClose();
}, [nameInput, callLink, onUpdateCallLinkName, onClose]);
return (
<Modal
modalName="CallLinkAddNameModal"
i18n={i18n}
hasXButton
noEscapeClose
noMouseClose
title={i18n('icu:CallLinkAddNameModal__Title')}
onClose={onClose}
moduleClassName="CallLinkAddNameModal"
modalFooter={
<>
<Button onClick={onClose} variant={ButtonVariant.Secondary}>
{i18n('icu:cancel')}
</Button>
<Button type="submit" form={formId} variant={ButtonVariant.Primary}>
{i18n('icu:save')}
</Button>
</>
}
>
<form
id={formId}
onSubmit={handleSubmit}
className="CallLinkAddNameModal__Row"
>
<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')
: callLink.name
}
/>
<label htmlFor={nameId} className="CallLinkAddNameModal__SrOnly">
{i18n('icu:CallLinkAddNameModal__NameLabel')}
</label>
<Input
i18n={i18n}
id={nameId}
value={nameInput}
placeholder={i18n('icu:CallLinkAddNameModal__NameLabel')}
autoFocus
onChange={handleNameInputChange}
moduleClassName="CallLinkAddNameModal__Input"
/>
</form>
</Modal>
);
}

View file

@ -20,7 +20,7 @@ export default {
callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY, callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY,
onClose: action('onClose'), onClose: action('onClose'),
onCopyCallLink: action('onCopyCallLink'), onCopyCallLink: action('onCopyCallLink'),
onUpdateCallLinkName: action('onUpdateCallLinkName'), onOpenCallLinkAddNameModal: action('onOpenCallLinkAddNameModal'),
onUpdateCallLinkRestrictions: action('onUpdateCallLinkRestrictions'), onUpdateCallLinkRestrictions: action('onUpdateCallLinkRestrictions'),
onShareCallLinkViaSignal: action('onShareCallLinkViaSignal'), onShareCallLinkViaSignal: action('onShareCallLinkViaSignal'),
onStartCallLinkLobby: action('onStartCallLinkLobby'), onStartCallLinkLobby: action('onStartCallLinkLobby'),

View file

@ -1,7 +1,8 @@
// Copyright 2024 Signal Messenger, LLC // Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React, { useCallback, useMemo, useState } from 'react'; import type { ReactNode } from 'react';
import React, { useMemo, useState } from 'react';
import { v4 as generateUuid } from 'uuid'; import { v4 as generateUuid } from 'uuid';
import { Modal } from './Modal'; import { Modal } from './Modal';
import type { LocalizerType } from '../types/I18N'; import type { LocalizerType } from '../types/I18N';
@ -10,19 +11,67 @@ import {
toCallLinkRestrictions, toCallLinkRestrictions,
type CallLinkType, type CallLinkType,
} from '../types/CallLink'; } from '../types/CallLink';
import { Input } from './Input';
import { Select } from './Select'; import { Select } from './Select';
import { linkCallRoute } from '../util/signalRoutes'; import { linkCallRoute } from '../util/signalRoutes';
import { Button, ButtonSize, ButtonVariant } from './Button'; import { Button, ButtonSize, ButtonVariant } from './Button';
import { Avatar, AvatarSize } from './Avatar'; import { Avatar, AvatarSize } from './Avatar';
import { formatUrlWithoutProtocol } from '../util/url';
const CallLinkEditModalRowIconClasses = {
Edit: 'CallLinkEditModal__RowIcon--Edit',
Approve: 'CallLinkEditModal__RowIcon--Approve',
Copy: 'CallLinkEditModal__RowIcon--Copy',
Share: 'CallLinkEditModal__RowIcon--Share',
} as const;
function RowIcon({
icon,
}: {
icon: keyof typeof CallLinkEditModalRowIconClasses;
}) {
return (
<i
role="presentation"
className={`CallLinkEditModal__RowIcon ${CallLinkEditModalRowIconClasses[icon]}`}
/>
);
}
function RowText({ children }: { children: ReactNode }) {
return <div className="CallLinkEditModal__RowLabel">{children}</div>;
}
function Row({ children }: { children: ReactNode }) {
return <div className="CallLinkEditModal__Row">{children}</div>;
}
function RowButton({
onClick,
children,
}: {
onClick: () => void;
children: ReactNode;
}) {
return (
<button
className="CallLinkEditModal__RowButton"
type="button"
onClick={onClick}
>
{children}
</button>
);
}
function Hr() {
return <hr className="CallLinkEditModal__Hr" />;
}
export type CallLinkEditModalProps = { export type CallLinkEditModalProps = {
i18n: LocalizerType; i18n: LocalizerType;
callLink: CallLinkType; callLink: CallLinkType;
onClose: () => void; onClose: () => void;
onCopyCallLink: () => void; onCopyCallLink: () => void;
onUpdateCallLinkName: (name: string) => void; onOpenCallLinkAddNameModal: () => void;
onUpdateCallLinkRestrictions: (restrictions: CallLinkRestrictions) => void; onUpdateCallLinkRestrictions: (restrictions: CallLinkRestrictions) => void;
onShareCallLinkViaSignal: () => void; onShareCallLinkViaSignal: () => void;
onStartCallLinkLobby: () => void; onStartCallLinkLobby: () => void;
@ -33,64 +82,32 @@ export function CallLinkEditModal({
callLink, callLink,
onClose, onClose,
onCopyCallLink, onCopyCallLink,
onUpdateCallLinkName, onOpenCallLinkAddNameModal,
onUpdateCallLinkRestrictions, onUpdateCallLinkRestrictions,
onShareCallLinkViaSignal, onShareCallLinkViaSignal,
onStartCallLinkLobby, onStartCallLinkLobby,
}: CallLinkEditModalProps): JSX.Element { }: CallLinkEditModalProps): JSX.Element {
const { name: savedName, restrictions: savedRestrictions } = callLink;
const [nameId] = useState(() => generateUuid());
const [restrictionsId] = 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(() => { const callLinkWebUrl = useMemo(() => {
return formatUrlWithoutProtocol( return linkCallRoute.toWebUrl({ key: callLink.rootKey }).toString();
linkCallRoute.toWebUrl({ key: callLink.rootKey })
);
}, [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 ( return (
<Modal <Modal
i18n={i18n} i18n={i18n}
modalName="CallLinkEditModal" modalName="CallLinkEditModal"
moduleClassName="CallLinkEditModal" moduleClassName="CallLinkEditModal"
title={i18n('icu:CallLinkEditModal__Title')} title={i18n('icu:CallLinkEditModal__Title')}
hasXButton noEscapeClose
onClose={() => { noMouseClose
// Save the modal in case the user hits escape padded={false}
onSaveName(nameInput); modalFooter={
onClose(); <Button type="submit" variant={ButtonVariant.Primary} onClick={onClose}>
}} {i18n('icu:done')}
</Button>
}
onClose={onClose}
> >
<div className="CallLinkEditModal__Header"> <div className="CallLinkEditModal__Header">
<Avatar <Avatar
@ -101,43 +118,30 @@ export function CallLinkEditModal({
acceptedMessageRequest acceptedMessageRequest
isMe={false} isMe={false}
sharedGroupNames={[]} sharedGroupNames={[]}
title={callLink.name ?? i18n('icu:calling__call-link-default-title')} title={
callLink.name === ''
? i18n('icu:calling__call-link-default-title')
: callLink.name
}
/> />
<div className="CallLinkEditModal__Header__Details"> <div className="CallLinkEditModal__Header__Details">
<label htmlFor={nameId} className="CallLinkEditModal__SrOnly"> <div className="CallLinkEditModal__Header__Title">
{i18n('icu:CallLinkEditModal__InputLabel--Name--SrOnly')} {callLink.name === ''
</label>
<Input
moduleClassName="CallLinkEditModal__Input--Name"
i18n={i18n}
value={
nameInput === '' && !nameTouched
? i18n('icu:calling__call-link-default-title') ? i18n('icu:calling__call-link-default-title')
: nameInput : callLink.name}
} </div>
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 <button
className="CallLinkEditModal__CopyUrlTextButton" className="CallLinkEditModal__Header__CallLinkButton"
type="button" type="button"
onClick={onCopyCallLink} onClick={onCopyCallLink}
aria-label={i18n('icu:CallLinkDetails__CopyLink')} aria-label={i18n('icu:CallLinkDetails__CopyLink')}
> >
<div className="CallLinkEditModal__Header__CallLinkButton__Text">
{callLinkWebUrl} {callLinkWebUrl}
</div>
</button> </button>
</div>
<div className="CallLinkEditModal__Header__Actions">
<Button <Button
onClick={onStartCallLinkLobby} onClick={onStartCallLinkLobby}
size={ButtonSize.Small} size={ButtonSize.Small}
@ -148,67 +152,62 @@ export function CallLinkEditModal({
</Button> </Button>
</div> </div>
</div> </div>
</div>
<div <Hr />
className="CallLinkEditModal__ApproveAllMembers__Row"
// For testing, to easily check the restrictions saved <RowButton onClick={onOpenCallLinkAddNameModal}>
data-restrictions={savedRestrictions} <Row>
> <RowIcon icon="Edit" />
<label <RowText>{i18n('icu:CallLinkEditModal__AddCallNameLabel')}</RowText>
htmlFor={restrictionsId} </Row>
className="CallLinkEditModal__ApproveAllMembers__Label" </RowButton>
>
<Row>
<RowIcon icon="Approve" />
<RowText>
<label htmlFor={restrictionsId}>
{i18n('icu:CallLinkEditModal__InputLabel--ApproveAllMembers')} {i18n('icu:CallLinkEditModal__InputLabel--ApproveAllMembers')}
</label> </label>
</RowText>
<Select <Select
id={restrictionsId} id={restrictionsId}
value={restrictionsInput} value={String(callLink.restrictions)}
moduleClassName="CallLinkEditModal__RowSelect"
options={[ options={[
{ {
value: CallLinkRestrictions.None, value: String(CallLinkRestrictions.None),
text: i18n( text: i18n(
'icu:CallLinkEditModal__ApproveAllMembers__Option--Off' 'icu:CallLinkEditModal__ApproveAllMembers__Option--Off'
), ),
}, },
{ {
value: CallLinkRestrictions.AdminApproval, value: String(CallLinkRestrictions.AdminApproval),
text: i18n( text: i18n(
'icu:CallLinkEditModal__ApproveAllMembers__Option--On' 'icu:CallLinkEditModal__ApproveAllMembers__Option--On'
), ),
}, },
]} ]}
onChange={value => { onChange={value => {
const newRestrictions = toCallLinkRestrictions(value); onUpdateCallLinkRestrictions(toCallLinkRestrictions(value));
setRestrictionsInput(newRestrictions);
onSaveRestrictions(newRestrictions);
}} }}
/> />
</div> </Row>
<button <Hr />
type="button"
className="CallLinkEditModal__ActionButton"
onClick={onCopyCallLink}
>
<i
role="presentation"
className="CallLinkEditModal__ActionButton__Icon CallLinkEditModal__ActionButton__Icon--Copy"
/>
{i18n('icu:CallLinkDetails__CopyLink')}
</button>
<button <RowButton onClick={onCopyCallLink}>
type="button" <Row>
className="CallLinkEditModal__ActionButton" <RowIcon icon="Copy" />
onClick={onShareCallLinkViaSignal} <RowText>{i18n('icu:CallLinkDetails__CopyLink')}</RowText>
> </Row>
<i </RowButton>
role="presentation"
className="CallLinkEditModal__ActionButton__Icon CallLinkEditModal__ActionButton__Icon--Share" <RowButton onClick={onShareCallLinkViaSignal}>
/> <Row>
{i18n('icu:CallLinkDetails__ShareLinkViaSignal')} <RowIcon icon="Share" />
</button> <RowText>{i18n('icu:CallLinkDetails__ShareLinkViaSignal')}</RowText>
</Row>
</RowButton>
</Modal> </Modal>
); );
} }

View file

@ -76,7 +76,7 @@ export function EditNicknameAndNoteModal({
}, [givenName, familyName, note]); }, [givenName, familyName, note]);
const handleSubmit = useCallback( const handleSubmit = useCallback(
(event: MouseEvent | FormEvent) => { (event: FormEvent) => {
event.preventDefault(); event.preventDefault();
if (formResult.success) { if (formResult.success) {
onSave(formResult.data); onSave(formResult.data);
@ -104,7 +104,6 @@ export function EditNicknameAndNoteModal({
type="submit" type="submit"
form={formId} form={formId}
aria-disabled={!formResult.success} aria-disabled={!formResult.success}
onClick={handleSubmit}
> >
{i18n('icu:save')} {i18n('icu:save')}
</Button> </Button>
@ -124,7 +123,7 @@ export function EditNicknameAndNoteModal({
theme={undefined} theme={undefined}
/> />
</div> </div>
<form onSubmit={handleSubmit}> <form id={formId} onSubmit={handleSubmit}>
<label <label
htmlFor={givenNameId} htmlFor={givenNameId}
className="EditNicknameAndNoteModal__Label" className="EditNicknameAndNoteModal__Label"

View file

@ -29,6 +29,9 @@ export type PropsType = {
// AddUserToAnotherGroupModal // AddUserToAnotherGroupModal
addUserToAnotherGroupModalContactId: string | undefined; addUserToAnotherGroupModalContactId: string | undefined;
renderAddUserToAnotherGroup: () => JSX.Element; renderAddUserToAnotherGroup: () => JSX.Element;
// CallLinkAddNameModal
callLinkAddNameModalRoomId: string | null;
renderCallLinkAddNameModal: () => JSX.Element;
// CallLinkEditModal // CallLinkEditModal
callLinkEditModalRoomId: string | null; callLinkEditModalRoomId: string | null;
renderCallLinkEditModal: () => JSX.Element; renderCallLinkEditModal: () => JSX.Element;
@ -105,6 +108,9 @@ export function GlobalModalContainer({
// AddUserToAnotherGroupModal // AddUserToAnotherGroupModal
addUserToAnotherGroupModalContactId, addUserToAnotherGroupModalContactId,
renderAddUserToAnotherGroup, renderAddUserToAnotherGroup,
// CallLinkAddNameModal
callLinkAddNameModalRoomId,
renderCallLinkAddNameModal,
// CallLinkEditModal // CallLinkEditModal
callLinkEditModalRoomId, callLinkEditModalRoomId,
renderCallLinkEditModal, renderCallLinkEditModal,
@ -194,6 +200,10 @@ export function GlobalModalContainer({
return renderAddUserToAnotherGroup(); return renderAddUserToAnotherGroup();
} }
if (callLinkAddNameModalRoomId) {
return renderCallLinkAddNameModal();
}
if (callLinkEditModalRoomId) { if (callLinkEditModalRoomId) {
return renderCallLinkEditModal(); return renderCallLinkEditModal();
} }

View file

@ -18,6 +18,7 @@ import { useRefMerger } from '../hooks/useRefMerger';
import { byteLength } from '../Bytes'; import { byteLength } from '../Bytes';
export type PropsType = { export type PropsType = {
autoFocus?: boolean;
countBytes?: (value: string) => number; countBytes?: (value: string) => number;
countLength?: (value: string) => number; countLength?: (value: string) => number;
disabled?: boolean; disabled?: boolean;
@ -63,6 +64,7 @@ export const Input = forwardRef<
PropsType PropsType
>(function InputInner( >(function InputInner(
{ {
autoFocus,
countBytes = byteLength, countBytes = byteLength,
countLength = grapheme.count, countLength = grapheme.count,
disabled, disabled,
@ -206,6 +208,7 @@ export const Input = forwardRef<
const isTextarea = expandable || forceTextarea; const isTextarea = expandable || forceTextarea;
const inputProps = { const inputProps = {
autoFocus,
className: classNames( className: classNames(
getClassName('__input'), getClassName('__input'),
icon && getClassName('__input--with-icon'), icon && getClassName('__input--with-icon'),

View file

@ -43,6 +43,7 @@ type PropsType = {
export type ModalPropsType = PropsType & { export type ModalPropsType = PropsType & {
noTransform?: boolean; noTransform?: boolean;
noEscapeClose?: boolean;
noMouseClose?: boolean; noMouseClose?: boolean;
theme?: Theme; theme?: Theme;
}; };
@ -57,6 +58,7 @@ export function Modal({
modalFooter, modalFooter,
modalHeaderChildren, modalHeaderChildren,
moduleClassName, moduleClassName,
noEscapeClose,
noMouseClose, noMouseClose,
onBackButtonClick, onBackButtonClick,
onClose = noop, onClose = noop,
@ -114,6 +116,7 @@ export function Modal({
<ModalHost <ModalHost
modalName={modalName} modalName={modalName}
moduleClassName={moduleClassName} moduleClassName={moduleClassName}
noEscapeClose={noEscapeClose}
noMouseClose={noMouseClose} noMouseClose={noMouseClose}
onClose={close} onClose={close}
onEscape={onBackButtonClick} onEscape={onBackButtonClick}

View file

@ -27,6 +27,7 @@ export type PropsType = Readonly<{
children: React.ReactElement; children: React.ReactElement;
modalName: string; modalName: string;
moduleClassName?: string; moduleClassName?: string;
noEscapeClose?: boolean;
noMouseClose?: boolean; noMouseClose?: boolean;
onClose: () => unknown; onClose: () => unknown;
onEscape?: () => unknown; onEscape?: () => unknown;
@ -40,6 +41,7 @@ export const ModalHost = React.memo(function ModalHostInner({
children, children,
modalName, modalName,
moduleClassName, moduleClassName,
noEscapeClose,
noMouseClose, noMouseClose,
onClose, onClose,
onEscape, onEscape,
@ -72,7 +74,7 @@ export const ModalHost = React.memo(function ModalHostInner({
}; };
}, [modalContainer]); }, [modalContainer]);
useEscapeHandling(onEscape || onClose); useEscapeHandling(noEscapeClose ? noop : onEscape || onClose);
useEffect(() => { useEffect(() => {
if (noMouseClose) { if (noMouseClose) {
return noop; return noop;

View file

@ -91,6 +91,7 @@ type MigrateToGV2PropsType = ReadonlyDeep<{
export type GlobalModalsStateType = ReadonlyDeep<{ export type GlobalModalsStateType = ReadonlyDeep<{
addUserToAnotherGroupModalContactId?: string; addUserToAnotherGroupModalContactId?: string;
aboutContactModalContactId?: string; aboutContactModalContactId?: string;
callLinkAddNameModalRoomId: string | null;
callLinkEditModalRoomId: string | null; callLinkEditModalRoomId: string | null;
contactModalState?: ContactModalStateType; contactModalState?: ContactModalStateType;
deleteMessagesProps?: DeleteMessagesPropsType; deleteMessagesProps?: DeleteMessagesPropsType;
@ -143,6 +144,8 @@ export const TOGGLE_PROFILE_EDITOR_ERROR =
const TOGGLE_SAFETY_NUMBER_MODAL = 'globalModals/TOGGLE_SAFETY_NUMBER_MODAL'; const TOGGLE_SAFETY_NUMBER_MODAL = 'globalModals/TOGGLE_SAFETY_NUMBER_MODAL';
const TOGGLE_ADD_USER_TO_ANOTHER_GROUP_MODAL = const TOGGLE_ADD_USER_TO_ANOTHER_GROUP_MODAL =
'globalModals/TOGGLE_ADD_USER_TO_ANOTHER_GROUP_MODAL'; 'globalModals/TOGGLE_ADD_USER_TO_ANOTHER_GROUP_MODAL';
const TOGGLE_CALL_LINK_ADD_NAME_MODAL =
'globalModals/TOGGLE_CALL_LINK_ADD_NAME_MODAL';
const TOGGLE_CALL_LINK_EDIT_MODAL = 'globalModals/TOGGLE_CALL_LINK_EDIT_MODAL'; const TOGGLE_CALL_LINK_EDIT_MODAL = 'globalModals/TOGGLE_CALL_LINK_EDIT_MODAL';
const TOGGLE_ABOUT_MODAL = 'globalModals/TOGGLE_ABOUT_MODAL'; const TOGGLE_ABOUT_MODAL = 'globalModals/TOGGLE_ABOUT_MODAL';
const TOGGLE_SIGNAL_CONNECTIONS_MODAL = const TOGGLE_SIGNAL_CONNECTIONS_MODAL =
@ -244,6 +247,11 @@ type ToggleAddUserToAnotherGroupModalActionType = ReadonlyDeep<{
payload: string | undefined; payload: string | undefined;
}>; }>;
type ToggleCallLinkAddNameModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_CALL_LINK_ADD_NAME_MODAL;
payload: string | null;
}>;
type ToggleCallLinkEditModalActionType = ReadonlyDeep<{ type ToggleCallLinkEditModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_CALL_LINK_EDIT_MODAL; type: typeof TOGGLE_CALL_LINK_EDIT_MODAL;
payload: string | null; payload: string | null;
@ -374,6 +382,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| StartMigrationToGV2ActionType | StartMigrationToGV2ActionType
| ToggleAboutContactModalActionType | ToggleAboutContactModalActionType
| ToggleAddUserToAnotherGroupModalActionType | ToggleAddUserToAnotherGroupModalActionType
| ToggleCallLinkAddNameModalActionType
| ToggleCallLinkEditModalActionType | ToggleCallLinkEditModalActionType
| ToggleConfirmationModalActionType | ToggleConfirmationModalActionType
| ToggleDeleteMessagesModalActionType | ToggleDeleteMessagesModalActionType
@ -414,6 +423,7 @@ export const actions = {
showWhatsNewModal, showWhatsNewModal,
toggleAboutContactModal, toggleAboutContactModal,
toggleAddUserToAnotherGroupModal, toggleAddUserToAnotherGroupModal,
toggleCallLinkAddNameModal,
toggleCallLinkEditModal, toggleCallLinkEditModal,
toggleConfirmationModal, toggleConfirmationModal,
toggleDeleteMessagesModal, toggleDeleteMessagesModal,
@ -711,6 +721,15 @@ function toggleAddUserToAnotherGroupModal(
}; };
} }
function toggleCallLinkAddNameModal(
roomId: string | null
): ToggleCallLinkAddNameModalActionType {
return {
type: TOGGLE_CALL_LINK_ADD_NAME_MODAL,
payload: roomId,
};
}
function toggleCallLinkEditModal( function toggleCallLinkEditModal(
roomId: string | null roomId: string | null
): ToggleCallLinkEditModalActionType { ): ToggleCallLinkEditModalActionType {
@ -935,6 +954,7 @@ function copyOverMessageAttributesIntoForwardMessages(
export function getEmptyState(): GlobalModalsStateType { export function getEmptyState(): GlobalModalsStateType {
return { return {
hasConfirmationModal: false, hasConfirmationModal: false,
callLinkAddNameModalRoomId: null,
callLinkEditModalRoomId: null, callLinkEditModalRoomId: null,
editNicknameAndNoteModalProps: null, editNicknameAndNoteModalProps: null,
isProfileEditorVisible: false, isProfileEditorVisible: false,
@ -1049,6 +1069,13 @@ export function reducer(
}; };
} }
if (action.type === TOGGLE_CALL_LINK_ADD_NAME_MODAL) {
return {
...state,
callLinkAddNameModalRoomId: action.payload,
};
}
if (action.type === TOGGLE_CALL_LINK_EDIT_MODAL) { if (action.type === TOGGLE_CALL_LINK_EDIT_MODAL) {
return { return {
...state, ...state,

View file

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

View file

@ -0,0 +1,60 @@
// 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 { 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 { getCallLinkAddNameModalRoomId } from '../selectors/globalModals';
import { strictAssert } from '../../util/assert';
import { isCallLinksCreateEnabled } from '../../util/callLinks';
import { CallLinkAddNameModal } from '../../components/CallLinkAddNameModal';
export const SmartCallLinkAddNameModal = memo(
function SmartCallLinkAddNameModal(): JSX.Element | null {
strictAssert(isCallLinksCreateEnabled(), 'Call links creation is disabled');
const roomId = useSelector(getCallLinkAddNameModalRoomId);
strictAssert(roomId, 'Expected roomId to be set');
const i18n = useSelector(getIntl);
const callLinkSelector = useSelector(getCallLinkSelector);
const { updateCallLinkName } = useCallingActions();
const { toggleCallLinkAddNameModal } = useGlobalModalActions();
const callLink = useMemo(() => {
return callLinkSelector(roomId);
}, [callLinkSelector, roomId]);
const handleClose = useCallback(() => {
toggleCallLinkAddNameModal(null);
}, [toggleCallLinkAddNameModal]);
const handleUpdateCallLinkName = useCallback(
(newName: string) => {
updateCallLinkName(roomId, newName);
},
[roomId, updateCallLinkName]
);
if (!callLink) {
log.error(
'SmartCallLinkEditModal: No call link found for roomId',
roomId
);
return null;
}
return (
<CallLinkAddNameModal
i18n={i18n}
callLink={callLink}
onClose={handleClose}
onUpdateCallLinkName={handleUpdateCallLinkName}
/>
);
}
);

View file

@ -26,13 +26,13 @@ export const SmartCallLinkEditModal = memo(
const i18n = useSelector(getIntl); const i18n = useSelector(getIntl);
const callLinkSelector = useSelector(getCallLinkSelector); const callLinkSelector = useSelector(getCallLinkSelector);
const { updateCallLinkRestrictions, startCallLinkLobby } =
useCallingActions();
const { const {
updateCallLinkName, toggleCallLinkAddNameModal,
updateCallLinkRestrictions, toggleCallLinkEditModal,
startCallLinkLobby, showShareCallLinkViaSignal,
} = useCallingActions(); } = useGlobalModalActions();
const { toggleCallLinkEditModal, showShareCallLinkViaSignal } =
useGlobalModalActions();
const callLink = useMemo(() => { const callLink = useMemo(() => {
return callLinkSelector(roomId); return callLinkSelector(roomId);
@ -52,12 +52,9 @@ export const SmartCallLinkEditModal = memo(
drop(copyCallLink(callLinkWebUrl)); drop(copyCallLink(callLinkWebUrl));
}, [callLink]); }, [callLink]);
const handleUpdateCallLinkName = useCallback( const handleOpenCallLinkAddNameModal = useCallback(() => {
(newName: string) => { toggleCallLinkAddNameModal(roomId);
updateCallLinkName(roomId, newName); }, [roomId, toggleCallLinkAddNameModal]);
},
[roomId, updateCallLinkName]
);
const handleUpdateCallLinkRestrictions = useCallback( const handleUpdateCallLinkRestrictions = useCallback(
(newRestrictions: CallLinkRestrictions) => { (newRestrictions: CallLinkRestrictions) => {
@ -91,7 +88,7 @@ export const SmartCallLinkEditModal = memo(
callLink={callLink} callLink={callLink}
onClose={handleClose} onClose={handleClose}
onCopyCallLink={handleCopyCallLink} onCopyCallLink={handleCopyCallLink}
onUpdateCallLinkName={handleUpdateCallLinkName} onOpenCallLinkAddNameModal={handleOpenCallLinkAddNameModal}
onUpdateCallLinkRestrictions={handleUpdateCallLinkRestrictions} onUpdateCallLinkRestrictions={handleUpdateCallLinkRestrictions}
onShareCallLinkViaSignal={handleShareCallLinkViaSignal} onShareCallLinkViaSignal={handleShareCallLinkViaSignal}
onStartCallLinkLobby={handleStartCallLinkLobby} onStartCallLinkLobby={handleStartCallLinkLobby}

View file

@ -27,6 +27,11 @@ import { getGlobalModalsState } from '../selectors/globalModals';
import { SmartEditNicknameAndNoteModal } from './EditNicknameAndNoteModal'; import { SmartEditNicknameAndNoteModal } from './EditNicknameAndNoteModal';
import { SmartNotePreviewModal } from './NotePreviewModal'; import { SmartNotePreviewModal } from './NotePreviewModal';
import { SmartCallLinkEditModal } from './CallLinkEditModal'; import { SmartCallLinkEditModal } from './CallLinkEditModal';
import { SmartCallLinkAddNameModal } from './CallLinkAddNameModal';
function renderCallLinkAddNameModal(): JSX.Element {
return <SmartCallLinkAddNameModal />;
}
function renderCallLinkEditModal(): JSX.Element { function renderCallLinkEditModal(): JSX.Element {
return <SmartCallLinkEditModal />; return <SmartCallLinkEditModal />;
@ -95,6 +100,7 @@ export const SmartGlobalModalContainer = memo(
const { const {
aboutContactModalContactId, aboutContactModalContactId,
addUserToAnotherGroupModalContactId, addUserToAnotherGroupModalContactId,
callLinkAddNameModalRoomId,
callLinkEditModalRoomId, callLinkEditModalRoomId,
contactModalState, contactModalState,
deleteMessagesProps, deleteMessagesProps,
@ -174,6 +180,7 @@ export const SmartGlobalModalContainer = memo(
addUserToAnotherGroupModalContactId={ addUserToAnotherGroupModalContactId={
addUserToAnotherGroupModalContactId addUserToAnotherGroupModalContactId
} }
callLinkAddNameModalRoomId={callLinkAddNameModalRoomId}
callLinkEditModalRoomId={callLinkEditModalRoomId} callLinkEditModalRoomId={callLinkEditModalRoomId}
contactModalState={contactModalState} contactModalState={contactModalState}
editHistoryMessages={editHistoryMessages} editHistoryMessages={editHistoryMessages}
@ -197,6 +204,7 @@ export const SmartGlobalModalContainer = memo(
isWhatsNewVisible={isWhatsNewVisible} isWhatsNewVisible={isWhatsNewVisible}
renderAboutContactModal={renderAboutContactModal} renderAboutContactModal={renderAboutContactModal}
renderAddUserToAnotherGroup={renderAddUserToAnotherGroup} renderAddUserToAnotherGroup={renderAddUserToAnotherGroup}
renderCallLinkAddNameModal={renderCallLinkAddNameModal}
renderCallLinkEditModal={renderCallLinkEditModal} renderCallLinkEditModal={renderCallLinkEditModal}
renderContactModal={renderContactModal} renderContactModal={renderContactModal}
renderEditHistoryMessagesModal={renderEditHistoryMessagesModal} renderEditHistoryMessagesModal={renderEditHistoryMessagesModal}

View file

@ -42,23 +42,29 @@ describe('calling/callLinkAdmin', function (this: Mocha.Suite) {
.getByText('Create a Call Link') .getByText('Create a Call Link')
.click(); .click();
const callLinkItem = window.locator('.CallsList__Item[data-type="Adhoc"]'); const editModal = window.locator('.CallLinkEditModal');
await editModal.waitFor();
const modal = window.locator('.CallLinkEditModal'); const restrictionsInput = editModal.getByLabel('Approve all members');
await modal.waitFor();
const row = modal.locator('.CallLinkEditModal__ApproveAllMembers__Row'); await expect(restrictionsInput).toHaveJSProperty('value', '0');
await restrictionsInput.selectOption({ label: 'On' });
await expect(restrictionsInput).toHaveJSProperty('value', '1');
await expect(row).toHaveAttribute('data-restrictions', '0'); await editModal.locator('button', { hasText: 'Add call name' }).click();
const select = modal.locator('select'); const addNameModal = window.locator('.CallLinkAddNameModal');
await select.selectOption({ label: 'On' }); await addNameModal.waitFor();
await expect(row).toHaveAttribute('data-restrictions', '1');
const nameInput = modal.locator('.CallLinkEditModal__Input--Name__input'); const nameInput = addNameModal.getByLabel('Call name');
await nameInput.fill('New Name'); await nameInput.fill('New Name');
await nameInput.blur();
await expect(callLinkItem).toContainText('New Name'); const saveBtn = addNameModal.getByText('Save');
await saveBtn.click();
await editModal.waitFor();
const title = editModal.locator('.CallLinkEditModal__Header__Title');
await expect(title).toContainText('New Name');
}); });
}); });

View file

@ -37,7 +37,3 @@ export function urlPathFromComponents(
): string { ): string {
return `/${components.filter(Boolean).map(encodeURIComponent).join('/')}`; return `/${components.filter(Boolean).map(encodeURIComponent).join('/')}`;
} }
export function formatUrlWithoutProtocol(url: Readonly<URL>): string {
return `${url.hostname}${url.pathname}${url.search}${url.hash}`;
}