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",
"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__AddCallNameLabel": {
"messageformat": "Add call name",
"description": "Call Link Edit Modal > Add Call Name Button > Label"
},
"icu:CallLinkEditModal__InputLabel--ApproveAllMembers": {
"messageformat": "Approve all members",
"description": "Call Link Edit Modal > Approve All Members Checkbox > Label"
@ -7228,6 +7228,14 @@
"messageformat": "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": {
"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."

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
// SPDX-License-Identifier: AGPL-3.0-only
// Overriding default style
.module-Modal__body.CallLinkEditModal__body {
padding-inline: 12px 3px;
scrollbar-gutter: stable;
}
.CallLinkEditModal__SrOnly {
@include sr-only;
}
@ -13,67 +19,80 @@
}
.CallLinkEditModal__Header__Details {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
gap: 0;
min-width: 0; // fix overflow issue
}
// Overriding default style
.Input__container.CallLinkEditModal__Input--Name__container {
margin: 0;
.CallLinkEditModal__Header__Title {
@include font-body-1-bold;
}
.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;
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 {
.CallLinkEditModal__Row {
display: flex;
padding: 12px;
align-items: center;
margin-bottom: 18px;
gap: 16px;
}
.CallLinkEditModal__ApproveAllMembers__Label {
flex: 1;
}
.CallLinkEditModal__ActionButton {
.CallLinkEditModal__RowButton {
@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;
display: flex;
gap: 8px;
align-items: center;
padding-block: 8px;
width: 100%;
@include light-theme {
@ -84,20 +103,17 @@
}
}
.CallLinkEditModal__ActionButton__Icon {
.CallLinkEditModal__RowLabel {
@include font-body-1;
flex: 1;
}
.CallLinkEditModal__RowIcon {
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;
}
width: 20px;
height: 20px;
&::after {
content: '';
@ -110,30 +126,42 @@
}
}
.CallLinkEditModal__ActionButton__Icon--Copy {
@mixin CallLinkEditModal__RowIcon($iconPath) {
&::after {
@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 color-svg('../images/icons/v3/copy/copy.svg', $color-gray-15);
@include color-svg($iconPath, $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
);
}
}
.CallLinkEditModal__RowIcon--Edit {
@include CallLinkEditModal__RowIcon('../images/icons/v3/edit/edit.svg');
}
.CallLinkEditModal__RowIcon--Approve {
@include CallLinkEditModal__RowIcon(
'../images/icons/v3/person/person-check-compact.svg'
);
}
.CallLinkEditModal__RowIcon--Copy {
@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/CallingSelectPresentingSourcesModal.scss';
@import './components/CallingToast.scss';
@import './components/CallLinkAddNameModal.scss';
@import './components/CallLinkDetails.scss';
@import './components/CallLinkEditModal.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,
onClose: action('onClose'),
onCopyCallLink: action('onCopyCallLink'),
onUpdateCallLinkName: action('onUpdateCallLinkName'),
onOpenCallLinkAddNameModal: action('onOpenCallLinkAddNameModal'),
onUpdateCallLinkRestrictions: action('onUpdateCallLinkRestrictions'),
onShareCallLinkViaSignal: action('onShareCallLinkViaSignal'),
onStartCallLinkLobby: action('onStartCallLinkLobby'),

View file

@ -1,7 +1,8 @@
// Copyright 2024 Signal Messenger, LLC
// 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 { Modal } from './Modal';
import type { LocalizerType } from '../types/I18N';
@ -10,19 +11,67 @@ import {
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';
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 = {
i18n: LocalizerType;
callLink: CallLinkType;
onClose: () => void;
onCopyCallLink: () => void;
onUpdateCallLinkName: (name: string) => void;
onOpenCallLinkAddNameModal: () => void;
onUpdateCallLinkRestrictions: (restrictions: CallLinkRestrictions) => void;
onShareCallLinkViaSignal: () => void;
onStartCallLinkLobby: () => void;
@ -33,64 +82,32 @@ export function CallLinkEditModal({
callLink,
onClose,
onCopyCallLink,
onUpdateCallLinkName,
onOpenCallLinkAddNameModal,
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 })
);
return linkCallRoute.toWebUrl({ key: callLink.rootKey }).toString();
}, [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();
}}
noEscapeClose
noMouseClose
padded={false}
modalFooter={
<Button type="submit" variant={ButtonVariant.Primary} onClick={onClose}>
{i18n('icu:done')}
</Button>
}
onClose={onClose}
>
<div className="CallLinkEditModal__Header">
<Avatar
@ -101,114 +118,96 @@ export function CallLinkEditModal({
acceptedMessageRequest
isMe={false}
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">
<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 className="CallLinkEditModal__Header__Title">
{callLink.name === ''
? i18n('icu:calling__call-link-default-title')
: callLink.name}
</div>
<button
className="CallLinkEditModal__Header__CallLinkButton"
type="button"
onClick={onCopyCallLink}
aria-label={i18n('icu:CallLinkDetails__CopyLink')}
>
<div className="CallLinkEditModal__Header__CallLinkButton__Text">
{callLinkWebUrl}
</div>
</button>
</div>
<div className="CallLinkEditModal__Header__Actions">
<Button
onClick={onStartCallLinkLobby}
size={ButtonSize.Small}
variant={ButtonVariant.SecondaryAffirmative}
className="CallLinkEditModal__JoinButton"
>
{i18n('icu:CallLinkEditModal__JoinButtonLabel')}
</Button>
</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>
<Hr />
<RowButton onClick={onOpenCallLinkAddNameModal}>
<Row>
<RowIcon icon="Edit" />
<RowText>{i18n('icu:CallLinkEditModal__AddCallNameLabel')}</RowText>
</Row>
</RowButton>
<Row>
<RowIcon icon="Approve" />
<RowText>
<label htmlFor={restrictionsId}>
{i18n('icu:CallLinkEditModal__InputLabel--ApproveAllMembers')}
</label>
</RowText>
<Select
id={restrictionsId}
value={restrictionsInput}
value={String(callLink.restrictions)}
moduleClassName="CallLinkEditModal__RowSelect"
options={[
{
value: CallLinkRestrictions.None,
value: String(CallLinkRestrictions.None),
text: i18n(
'icu:CallLinkEditModal__ApproveAllMembers__Option--Off'
),
},
{
value: CallLinkRestrictions.AdminApproval,
value: String(CallLinkRestrictions.AdminApproval),
text: i18n(
'icu:CallLinkEditModal__ApproveAllMembers__Option--On'
),
},
]}
onChange={value => {
const newRestrictions = toCallLinkRestrictions(value);
setRestrictionsInput(newRestrictions);
onSaveRestrictions(newRestrictions);
onUpdateCallLinkRestrictions(toCallLinkRestrictions(value));
}}
/>
</div>
</Row>
<button
type="button"
className="CallLinkEditModal__ActionButton"
onClick={onCopyCallLink}
>
<i
role="presentation"
className="CallLinkEditModal__ActionButton__Icon CallLinkEditModal__ActionButton__Icon--Copy"
/>
{i18n('icu:CallLinkDetails__CopyLink')}
</button>
<Hr />
<button
type="button"
className="CallLinkEditModal__ActionButton"
onClick={onShareCallLinkViaSignal}
>
<i
role="presentation"
className="CallLinkEditModal__ActionButton__Icon CallLinkEditModal__ActionButton__Icon--Share"
/>
{i18n('icu:CallLinkDetails__ShareLinkViaSignal')}
</button>
<RowButton onClick={onCopyCallLink}>
<Row>
<RowIcon icon="Copy" />
<RowText>{i18n('icu:CallLinkDetails__CopyLink')}</RowText>
</Row>
</RowButton>
<RowButton onClick={onShareCallLinkViaSignal}>
<Row>
<RowIcon icon="Share" />
<RowText>{i18n('icu:CallLinkDetails__ShareLinkViaSignal')}</RowText>
</Row>
</RowButton>
</Modal>
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -91,6 +91,7 @@ type MigrateToGV2PropsType = ReadonlyDeep<{
export type GlobalModalsStateType = ReadonlyDeep<{
addUserToAnotherGroupModalContactId?: string;
aboutContactModalContactId?: string;
callLinkAddNameModalRoomId: string | null;
callLinkEditModalRoomId: string | null;
contactModalState?: ContactModalStateType;
deleteMessagesProps?: DeleteMessagesPropsType;
@ -143,6 +144,8 @@ 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_ADD_NAME_MODAL =
'globalModals/TOGGLE_CALL_LINK_ADD_NAME_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 =
@ -244,6 +247,11 @@ type ToggleAddUserToAnotherGroupModalActionType = ReadonlyDeep<{
payload: string | undefined;
}>;
type ToggleCallLinkAddNameModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_CALL_LINK_ADD_NAME_MODAL;
payload: string | null;
}>;
type ToggleCallLinkEditModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_CALL_LINK_EDIT_MODAL;
payload: string | null;
@ -374,6 +382,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| StartMigrationToGV2ActionType
| ToggleAboutContactModalActionType
| ToggleAddUserToAnotherGroupModalActionType
| ToggleCallLinkAddNameModalActionType
| ToggleCallLinkEditModalActionType
| ToggleConfirmationModalActionType
| ToggleDeleteMessagesModalActionType
@ -414,6 +423,7 @@ export const actions = {
showWhatsNewModal,
toggleAboutContactModal,
toggleAddUserToAnotherGroupModal,
toggleCallLinkAddNameModal,
toggleCallLinkEditModal,
toggleConfirmationModal,
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(
roomId: string | null
): ToggleCallLinkEditModalActionType {
@ -935,6 +954,7 @@ function copyOverMessageAttributesIntoForwardMessages(
export function getEmptyState(): GlobalModalsStateType {
return {
hasConfirmationModal: false,
callLinkAddNameModalRoomId: null,
callLinkEditModalRoomId: null,
editNicknameAndNoteModalProps: null,
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) {
return {
...state,

View file

@ -27,6 +27,11 @@ export const getCallLinkEditModalRoomId = createSelector(
({ callLinkEditModalRoomId }) => callLinkEditModalRoomId
);
export const getCallLinkAddNameModalRoomId = createSelector(
getGlobalModalsState,
({ callLinkAddNameModalRoomId }) => callLinkAddNameModalRoomId
);
export const getContactModalState = createSelector(
getGlobalModalsState,
({ 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 callLinkSelector = useSelector(getCallLinkSelector);
const { updateCallLinkRestrictions, startCallLinkLobby } =
useCallingActions();
const {
updateCallLinkName,
updateCallLinkRestrictions,
startCallLinkLobby,
} = useCallingActions();
const { toggleCallLinkEditModal, showShareCallLinkViaSignal } =
useGlobalModalActions();
toggleCallLinkAddNameModal,
toggleCallLinkEditModal,
showShareCallLinkViaSignal,
} = useGlobalModalActions();
const callLink = useMemo(() => {
return callLinkSelector(roomId);
@ -52,12 +52,9 @@ export const SmartCallLinkEditModal = memo(
drop(copyCallLink(callLinkWebUrl));
}, [callLink]);
const handleUpdateCallLinkName = useCallback(
(newName: string) => {
updateCallLinkName(roomId, newName);
},
[roomId, updateCallLinkName]
);
const handleOpenCallLinkAddNameModal = useCallback(() => {
toggleCallLinkAddNameModal(roomId);
}, [roomId, toggleCallLinkAddNameModal]);
const handleUpdateCallLinkRestrictions = useCallback(
(newRestrictions: CallLinkRestrictions) => {
@ -91,7 +88,7 @@ export const SmartCallLinkEditModal = memo(
callLink={callLink}
onClose={handleClose}
onCopyCallLink={handleCopyCallLink}
onUpdateCallLinkName={handleUpdateCallLinkName}
onOpenCallLinkAddNameModal={handleOpenCallLinkAddNameModal}
onUpdateCallLinkRestrictions={handleUpdateCallLinkRestrictions}
onShareCallLinkViaSignal={handleShareCallLinkViaSignal}
onStartCallLinkLobby={handleStartCallLinkLobby}

View file

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

View file

@ -42,23 +42,29 @@ describe('calling/callLinkAdmin', function (this: Mocha.Suite) {
.getByText('Create a Call Link')
.click();
const callLinkItem = window.locator('.CallsList__Item[data-type="Adhoc"]');
const editModal = window.locator('.CallLinkEditModal');
await editModal.waitFor();
const modal = window.locator('.CallLinkEditModal');
await modal.waitFor();
const restrictionsInput = editModal.getByLabel('Approve all members');
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');
await select.selectOption({ label: 'On' });
await expect(row).toHaveAttribute('data-restrictions', '1');
const addNameModal = window.locator('.CallLinkAddNameModal');
await addNameModal.waitFor();
const nameInput = modal.locator('.CallLinkEditModal__Input--Name__input');
const nameInput = addNameModal.getByLabel('Call 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 {
return `/${components.filter(Boolean).map(encodeURIComponent).join('/')}`;
}
export function formatUrlWithoutProtocol(url: Readonly<URL>): string {
return `${url.hostname}${url.pathname}${url.search}${url.hash}`;
}