From f19f0fb47d8285608ddc691f560cccf26ee01352 Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Mon, 10 Jun 2024 08:23:43 -0700 Subject: [PATCH] Init create/admin call links flow --- _locales/en/messages.json | 28 +++ package.json | 2 +- stylesheets/components/CallLinkEditModal.scss | 139 ++++++++++++ stylesheets/components/CallsTab.scss | 2 +- stylesheets/manifest.scss | 1 + ts/RemoteConfig.ts | 1 + ts/components/CallLinkEditModal.stories.tsx | 32 +++ ts/components/CallLinkEditModal.tsx | 214 ++++++++++++++++++ ts/components/CallsList.tsx | 165 ++++++++++---- ts/components/CallsTab.tsx | 6 + ts/components/GlobalModalContainer.tsx | 22 +- ts/components/Input.tsx | 3 + ts/services/calling.ts | 176 +++++++++++++- ts/sql/Interface.ts | 2 +- ts/sql/server/callLinks.ts | 9 +- ts/state/ducks/callHistory.ts | 4 +- ts/state/ducks/calling.ts | 106 ++++++++- ts/state/ducks/globalModals.ts | 72 ++++++ ts/state/selectors/globalModals.ts | 5 + ts/state/smart/CallLinkDetails.tsx | 34 +-- ts/state/smart/CallLinkEditModal.tsx | 101 +++++++++ ts/state/smart/CallsTab.tsx | 18 +- ts/state/smart/GlobalModalContainer.tsx | 8 + ts/test-mock/bootstrap.ts | 1 + ts/test-mock/calling/callLinkAdmin_test.ts | 64 ++++++ ts/textsecure/WebAPI.ts | 27 ++- ts/types/CallLink.ts | 5 +- ts/util/callLinks.ts | 46 +++- ts/util/numbers.ts | 59 +++++ ts/util/url.ts | 4 + yarn.lock | 49 +--- 31 files changed, 1256 insertions(+), 149 deletions(-) create mode 100644 stylesheets/components/CallLinkEditModal.scss create mode 100644 ts/components/CallLinkEditModal.stories.tsx create mode 100644 ts/components/CallLinkEditModal.tsx create mode 100644 ts/state/smart/CallLinkEditModal.tsx create mode 100644 ts/test-mock/calling/callLinkAdmin_test.ts create mode 100644 ts/util/numbers.ts diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 9298d6dce..1dbb9060c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -7080,6 +7080,10 @@ "messageformat": "No results for “{query}”", "description": "Calls Tab > Calls List > When no results found > With a search query" }, + "icu:CallsList__CreateCallLink": { + "messageformat": "Create a Call Link", + "description": "Calls Tab > Calls List > Create Call Link Button" + }, "icu:CallsList__ItemCallInfo--Incoming": { "messageformat": "Incoming", "description": "Calls Tab > Calls List > Call Item > Call Status > When call was accepted and was incoming" @@ -7164,6 +7168,30 @@ "messageformat": "Share link via Signal", "description": "Call History > Call Link Details > Share Link via Signal Button" }, + "icu:CallLinkEditModal__Title": { + "messageformat": "Call link details", + "description": "Call Link Edit Modal > Title" + }, + "icu:CallLinkEditModal__InputLabel--Name--SrOnly": { + "messageformat": "Name", + "description": "Call Link Edit Modal > Name Input > Label (for screenreaders)" + }, + "icu:CallLinkEditModal__JoinButtonLabel": { + "messageformat": "Join", + "description": "Call Link Edit Modal > Join Button > Label" + }, + "icu:CallLinkEditModal__InputLabel--ApproveAllMembers": { + "messageformat": "Approve all members", + "description": "Call Link Edit Modal > Approve All Members Checkbox > Label" + }, + "icu:CallLinkEditModal__ApproveAllMembers__Option--Off": { + "messageformat": "Off", + "description": "Call Link Edit Modal > Approve All Members Checkbox > Option > Off" + }, + "icu:CallLinkEditModal__ApproveAllMembers__Option--On": { + "messageformat": "On", + "description": "Call Link Edit Modal > Approve All Members Checkbox > Option > On" + }, "icu:TypingBubble__avatar--overflow-count": { "messageformat": "{count, plural, one {# other is} other {# others are}} typing.", "description": "Group chat multiple person typing indicator when space isn't available to show every avatar, this is the count of avatars hidden." diff --git a/package.json b/package.json index 22357cba2..f4f81d56c 100644 --- a/package.json +++ b/package.json @@ -211,7 +211,7 @@ "@formatjs/intl": "2.6.7", "@indutny/rezip-electron": "1.3.1", "@mixer/parallel-prettier": "2.0.3", - "@signalapp/mock-server": "6.5.0", + "@signalapp/mock-server": "6.6.0", "@storybook/addon-a11y": "7.4.5", "@storybook/addon-actions": "7.4.5", "@storybook/addon-controls": "7.4.5", diff --git a/stylesheets/components/CallLinkEditModal.scss b/stylesheets/components/CallLinkEditModal.scss new file mode 100644 index 000000000..ada82d175 --- /dev/null +++ b/stylesheets/components/CallLinkEditModal.scss @@ -0,0 +1,139 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.CallLinkEditModal__SrOnly { + @include sr-only; +} + +.CallLinkEditModal__Header { + display: flex; + gap: 16px; + align-items: center; + margin-bottom: 26px; +} + +.CallLinkEditModal__Header__Details { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; // fix overflow issue +} + +// Overriding default style +.Input__container.CallLinkEditModal__Input--Name__container { + margin: 0; +} + +.CallLinkEditModal__CallLinkAndJoinButton { + display: flex; + gap: 14px; + align-items: center; +} + +.CallLinkEditModal__CopyUrlTextButton { + @include button-reset; + border: none; + padding-block: 10px; + padding-inline: 8px; + border-radius: 6px; + flex: 1; + + // truncate with ellipsis + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + @include light-theme { + background: $color-gray-02; + color: $color-black; + } + @include dark-theme { + background: $color-gray-75; + color: $color-gray-15; + } +} + +.CallLinkEditModal__JoinButton { + @include font-body-1-bold; +} + +.CallLinkEditModal__ApproveAllMembers__Row { + display: flex; + align-items: center; + margin-bottom: 18px; +} + +.CallLinkEditModal__ApproveAllMembers__Label { + flex: 1; +} + +.CallLinkEditModal__ActionButton { + @include button-reset; + @include font-body-2; + display: flex; + gap: 8px; + align-items: center; + padding-block: 8px; + width: 100%; + + @include light-theme { + color: $color-black; + } + @include dark-theme { + color: $color-gray-15; + } +} + +.CallLinkEditModal__ActionButton__Icon { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 9999px; + + @include light-theme { + background: $color-gray-05; + } + @include dark-theme { + background: $color-gray-65; + } + + &::after { + content: ''; + display: block; + width: 20px; + height: 20px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + } +} + +.CallLinkEditModal__ActionButton__Icon--Copy { + &::after { + @include light-theme { + @include color-svg('../images/icons/v3/copy/copy.svg', $color-gray-75); + } + @include dark-theme { + @include color-svg('../images/icons/v3/copy/copy.svg', $color-gray-15); + } + } +} + +.CallLinkEditModal__ActionButton__Icon--Share { + &::after { + @include light-theme { + @include color-svg( + '../images/icons/v3/forward/forward.svg', + $color-gray-75 + ); + } + @include dark-theme { + @include color-svg( + '../images/icons/v3/forward/forward.svg', + $color-gray-15 + ); + } + } +} diff --git a/stylesheets/components/CallsTab.scss b/stylesheets/components/CallsTab.scss index 02fb08404..5a954cedd 100644 --- a/stylesheets/components/CallsTab.scss +++ b/stylesheets/components/CallsTab.scss @@ -161,7 +161,7 @@ @include NavTabs__Scroller; } -.CallsList__List--loading { +.CallsList__List--disableScrolling { overflow: hidden !important; } diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 7ea684758..3a4f4571f 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -51,6 +51,7 @@ @import './components/CallingSelectPresentingSourcesModal.scss'; @import './components/CallingToast.scss'; @import './components/CallLinkDetails.scss'; +@import './components/CallLinkEditModal.scss'; @import './components/CallingRaisedHandsList.scss'; @import './components/CallingRaisedHandsToasts.scss'; @import './components/CallingReactionsToasts.scss'; diff --git a/ts/RemoteConfig.ts b/ts/RemoteConfig.ts index 121aa9d71..2010c5e32 100644 --- a/ts/RemoteConfig.ts +++ b/ts/RemoteConfig.ts @@ -16,6 +16,7 @@ import { getCountryCode } from './types/PhoneNumber'; export type ConfigKeyType = | 'desktop.calling.adhoc' + | 'desktop.calling.adhoc.create' | 'desktop.clientExpiration' | 'desktop.backup.credentialFetch' | 'desktop.deleteSync.send' diff --git a/ts/components/CallLinkEditModal.stories.tsx b/ts/components/CallLinkEditModal.stories.tsx new file mode 100644 index 000000000..cb1f9c7c5 --- /dev/null +++ b/ts/components/CallLinkEditModal.stories.tsx @@ -0,0 +1,32 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { setupI18n } from '../util/setupI18n'; +import enMessages from '../../_locales/en/messages.json'; +import type { CallLinkEditModalProps } from './CallLinkEditModal'; +import { CallLinkEditModal } from './CallLinkEditModal'; +import type { ComponentMeta } from '../storybook/types'; +import { FAKE_CALL_LINK_WITH_ADMIN_KEY } from '../test-both/helpers/fakeCallLink'; + +const i18n = setupI18n('en', enMessages); + +export default { + title: 'Components/CallLinkEditModal', + component: CallLinkEditModal, + args: { + i18n, + callLink: FAKE_CALL_LINK_WITH_ADMIN_KEY, + onClose: action('onClose'), + onCopyCallLink: action('onCopyCallLink'), + onUpdateCallLinkName: action('onUpdateCallLinkName'), + onUpdateCallLinkRestrictions: action('onUpdateCallLinkRestrictions'), + onShareCallLinkViaSignal: action('onShareCallLinkViaSignal'), + onStartCallLinkLobby: action('onStartCallLinkLobby'), + }, +} satisfies ComponentMeta; + +export function Basic(args: CallLinkEditModalProps): JSX.Element { + return ; +} diff --git a/ts/components/CallLinkEditModal.tsx b/ts/components/CallLinkEditModal.tsx new file mode 100644 index 000000000..196a77d16 --- /dev/null +++ b/ts/components/CallLinkEditModal.tsx @@ -0,0 +1,214 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React, { useCallback, useMemo, useState } from 'react'; +import { v4 as generateUuid } from 'uuid'; +import { Modal } from './Modal'; +import type { LocalizerType } from '../types/I18N'; +import { + CallLinkRestrictions, + toCallLinkRestrictions, + type CallLinkType, +} from '../types/CallLink'; +import { Input } from './Input'; +import { Select } from './Select'; +import { linkCallRoute } from '../util/signalRoutes'; +import { Button, ButtonSize, ButtonVariant } from './Button'; +import { Avatar, AvatarSize } from './Avatar'; +import { formatUrlWithoutProtocol } from '../util/url'; + +export type CallLinkEditModalProps = { + i18n: LocalizerType; + callLink: CallLinkType; + onClose: () => void; + onCopyCallLink: () => void; + onUpdateCallLinkName: (name: string) => void; + onUpdateCallLinkRestrictions: (restrictions: CallLinkRestrictions) => void; + onShareCallLinkViaSignal: () => void; + onStartCallLinkLobby: () => void; +}; + +export function CallLinkEditModal({ + i18n, + callLink, + onClose, + onCopyCallLink, + onUpdateCallLinkName, + onUpdateCallLinkRestrictions, + onShareCallLinkViaSignal, + onStartCallLinkLobby, +}: CallLinkEditModalProps): JSX.Element { + const { name: savedName, restrictions: savedRestrictions } = callLink; + + const [nameId] = useState(() => generateUuid()); + const [restrictionsId] = useState(() => generateUuid()); + + const [nameInput, setNameInput] = useState(savedName); + const [restrictionsInput, setRestrictionsInput] = useState(savedRestrictions); + + // We only want to use the default name "Signal Call" as a value if the user + // modified the input and then chose that name. Doesn't revert when saved. + const [nameTouched, setNameTouched] = useState(false); + + const callLinkWebUrl = useMemo(() => { + return formatUrlWithoutProtocol( + linkCallRoute.toWebUrl({ key: callLink.rootKey }) + ); + }, [callLink.rootKey]); + + const onSaveName = useCallback( + (newName: string) => { + if (!nameTouched) { + return; + } + if (newName === savedName) { + return; + } + onUpdateCallLinkName(newName); + }, + [nameTouched, savedName, onUpdateCallLinkName] + ); + + const onSaveRestrictions = useCallback( + (newRestrictions: CallLinkRestrictions) => { + if (newRestrictions === savedRestrictions) { + return; + } + onUpdateCallLinkRestrictions(newRestrictions); + }, + [savedRestrictions, onUpdateCallLinkRestrictions] + ); + + return ( + { + // Save the modal in case the user hits escape + onSaveName(nameInput); + onClose(); + }} + > +
+ +
+ + { + setNameTouched(true); + setNameInput(value); + }} + onBlur={() => { + onSaveName(nameInput); + }} + onEnter={() => { + onSaveName(nameInput); + }} + placeholder={i18n('icu:calling__call-link-default-title')} + /> + +
+ + +
+
+
+ +
+ +