From 5570d6935bfffdfda36ea7de4e9826f2ec126e23 Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Wed, 12 Oct 2022 09:34:22 -0700 Subject: [PATCH] Added story preview and confirmation dialogs to send story Co-authored-by: Alvaro <110414366+alvaro-signal@users.noreply.github.com> --- stylesheets/components/SendStoryModal.scss | 39 ++++- ts/components/ConfirmDiscardDialog.tsx | 2 +- ts/components/ContextMenu.stories.tsx | 2 +- ts/components/ContextMenu.tsx | 61 ++++--- ts/components/MediaEditor.tsx | 18 ++- ts/components/SendStoryModal.stories.tsx | 8 + ts/components/SendStoryModal.tsx | 176 +++++++++++++++------ ts/components/Stories.tsx | 3 +- ts/components/StoriesSettingsModal.tsx | 53 ++++--- ts/components/StoryCreator.tsx | 1 + ts/components/TextStoryCreator.tsx | 28 +++- ts/hooks/useConfirmDiscard.tsx | 33 ++++ 12 files changed, 315 insertions(+), 109 deletions(-) create mode 100644 ts/hooks/useConfirmDiscard.tsx diff --git a/stylesheets/components/SendStoryModal.scss b/stylesheets/components/SendStoryModal.scss index 6b0ce1fdb..7ce1f28ec 100644 --- a/stylesheets/components/SendStoryModal.scss +++ b/stylesheets/components/SendStoryModal.scss @@ -18,6 +18,19 @@ } } + .module-SearchInput__container { + margin-left: 0; + margin-right: 0; + } + + &__story-preview { + height: 140px; + width: 80px; + border-radius: 12px; + margin: 0 auto 16px auto; + background-size: cover; + } + &__item--contact-or-conversation { height: 52px; padding: 0 6px; @@ -32,13 +45,17 @@ } &__new-story { - &__container { + &__button { + font-weight: 500; + padding: 5px 10px; &::before { @include color-svg('../images/icons/v2/plus-20.svg', $color-white); content: ''; height: 16px; - margin-right: 8px; width: 16px; + margin-right: 8px; + display: inline-block; + vertical-align: text-bottom; } } @@ -121,11 +138,13 @@ &__name { @include font-body-1-bold; + font-weight: 400; } &__description { @include font-body-2; - color: $color-gray-60; + font-size: 12px; + color: $color-gray-25; } &__checkbox { @@ -187,6 +206,9 @@ padding-right: 16px; user-select: none; flex: 1; + display: flex; + align-items: center; + min-height: 32px; } &__ok { @@ -232,4 +254,15 @@ width: 18px; } } + + // more specific to override StoryImage + .SendStoryModal { + &__story { + border-radius: 12px; + backdrop-filter: blur(90px); + &__image { + object-fit: contain; + } + } + } } diff --git a/ts/components/ConfirmDiscardDialog.tsx b/ts/components/ConfirmDiscardDialog.tsx index 034b196d6..c99f6e820 100644 --- a/ts/components/ConfirmDiscardDialog.tsx +++ b/ts/components/ConfirmDiscardDialog.tsx @@ -15,7 +15,7 @@ export const ConfirmDiscardDialog = ({ i18n, onClose, onDiscard, -}: PropsType): JSX.Element | null => { +}: PropsType): JSX.Element => { return ( => ({ }); export const Default = (): JSX.Element => { - return ; + return Menu; }; diff --git a/ts/components/ContextMenu.tsx b/ts/components/ContextMenu.tsx index f94541a5a..14fc51600 100644 --- a/ts/components/ContextMenu.tsx +++ b/ts/components/ContextMenu.tsx @@ -23,20 +23,29 @@ export type ContextMenuOptionType = { readonly value?: T; }; -export type PropsType = { - readonly ariaLabel?: string; - readonly children?: ReactNode; - readonly i18n: LocalizerType; - readonly menuOptions: ReadonlyArray>; - readonly moduleClassName?: string; - readonly onClick?: () => unknown; - readonly onMenuShowingChanged?: (value: boolean) => unknown; - readonly popperOptions?: Pick; - readonly theme?: Theme; - readonly title?: string; - readonly value?: T; +type RenderButtonProps = { + openMenu: (() => void) | ((ev: React.MouseEvent) => void); + onKeyDown: (ev: KeyboardEvent) => void; + isMenuShowing: boolean; + ref: React.Ref | null; }; +export type PropsType = Readonly<{ + ariaLabel?: string; + // contents of the button OR a function that will render the whole button + children?: ReactNode | ((props: RenderButtonProps) => JSX.Element); + i18n: LocalizerType; + menuOptions: ReadonlyArray>; + moduleClassName?: string; + button?: () => JSX.Element; + onClick?: () => unknown; + onMenuShowingChanged?: (value: boolean) => unknown; + popperOptions?: Pick; + theme?: Theme; + title?: string; + value?: T; +}>; + let closeCurrentOpenContextMenu: undefined | (() => unknown); // https://popper.js.org/docs/v2/virtual-elements/ @@ -167,13 +176,16 @@ export function ContextMenu({ const getClassName = getClassNamesFor('ContextMenu', moduleClassName); - return ( -
+ let buttonNode: ReactNode; + if (typeof children === 'function') { + buttonNode = (children as (props: RenderButtonProps) => JSX.Element)({ + openMenu: onClick || handleClick, + onKeyDown: handleKeyDown, + isMenuShowing, + ref: setReferenceElement, + }); + } else { + buttonNode = ( + ); + } + + return ( +
+ {buttonNode} {isMenuShowing && ( (); + const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n); + + const onTryClose = useCallback(() => { + confirmDiscardIf(caption !== '' || Boolean(image), onClose); + }, [confirmDiscardIf, caption, image, onClose]); + // Keyboard support useEffect(() => { if (!fabricCanvas) { @@ -207,7 +214,7 @@ export const MediaEditor = ({ // there's no easy way to prevent an ESC meant for the // sticker-picker from hitting this handler first if (!isStickerPopperOpen) { - onClose(); + onTryClose(); } } else { setEditMode(undefined); @@ -349,7 +356,7 @@ export const MediaEditor = ({ fabricCanvas, editMode, isStickerPopperOpen, - onClose, + onTryClose, redoIfPossible, undoIfPossible, ]); @@ -957,7 +964,7 @@ export const MediaEditor = ({ )}
+ {confirmDiscardModal}
, portal ); diff --git a/ts/components/SendStoryModal.stories.tsx b/ts/components/SendStoryModal.stories.tsx index 89892a178..e6d930324 100644 --- a/ts/components/SendStoryModal.stories.tsx +++ b/ts/components/SendStoryModal.stories.tsx @@ -16,6 +16,7 @@ import { getMyStories, getFakeDistributionListsWithMembers, } from '../test-both/helpers/getFakeDistributionLists'; +import { VIDEO_MP4 } from '../types/MIME'; const i18n = setupI18n('en', enMessages); @@ -28,6 +29,13 @@ export default { title: 'Components/SendStoryModal', component: SendStoryModal, argTypes: { + draftAttachment: { + defaultValue: { + contentType: VIDEO_MP4, + fileName: 'pixabay-Soap-Bubble-7141.mp4', + url: '/fixtures/pixabay-Soap-Bubble-7141.mp4', + }, + }, candidateConversations: { defaultValue: Array.from(Array(100), () => getDefaultConversation()), }, diff --git a/ts/components/SendStoryModal.tsx b/ts/components/SendStoryModal.tsx index 07b14dd5f..e98af7281 100644 --- a/ts/components/SendStoryModal.tsx +++ b/ts/components/SendStoryModal.tsx @@ -15,7 +15,7 @@ import type { StoryDistributionListWithMembersDataType } from '../types/Stories' import type { UUIDStringType } from '../types/UUID'; import { Alert } from './Alert'; import { Avatar, AvatarSize } from './Avatar'; -import { Button, ButtonVariant } from './Button'; +import { Button, ButtonSize, ButtonVariant } from './Button'; import { Checkbox } from './Checkbox'; import { ConfirmationDialog } from './ConfirmationDialog'; import { ContextMenu } from './ContextMenu'; @@ -31,8 +31,14 @@ import { PagedModal, ModalPage } from './Modal'; import { StoryDistributionListName } from './StoryDistributionListName'; import { Theme } from '../util/theme'; import { isNotNil } from '../util/isNotNil'; +import { StoryImage } from './StoryImage'; +import type { AttachmentType } from '../types/Attachment'; +import { useConfirmDiscard } from '../hooks/useConfirmDiscard'; +import { getStoryBackground } from '../util/getStoryBackground'; +import { makeObjectUrl, revokeObjectUrl } from '../types/VisualAttachment'; export type PropsType = { + draftAttachment: AttachmentType; candidateConversations: Array; distributionLists: Array; getPreferredBadge: PreferredBadgeSelectorType; @@ -114,6 +120,7 @@ function getListViewers( } export const SendStoryModal = ({ + draftAttachment, candidateConversations, distributionLists, getPreferredBadge, @@ -138,6 +145,8 @@ export const SendStoryModal = ({ }: PropsType): JSX.Element => { const [page, setPage] = useState(Page.SendStory); + const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n); + const [selectedListIds, setSelectedListIds] = useState>( new Set() ); @@ -267,6 +276,24 @@ export const SendStoryModal = ({ .join(', '); } + const [objectUrl, setObjectUrl] = useState(undefined); + + useEffect(() => { + let url: undefined | string; + + if (draftAttachment.url) { + setObjectUrl(draftAttachment.url); + } else if (draftAttachment.data) { + url = makeObjectUrl(draftAttachment.data, draftAttachment.contentType); + setObjectUrl(url); + } + return () => { + if (url) { + revokeObjectUrl(url); + } + }; + }, [setObjectUrl, draftAttachment]); + const modalCommonProps: Pick = { hasXButton: true, i18n, @@ -375,7 +402,11 @@ export const SendStoryModal = ({ setPage={setPage} setSelectedContacts={setSelectedContacts} toggleSignalConnectionsModal={toggleSignalConnectionsModal} - onBackButtonClick={() => setListIdToEdit(undefined)} + onBackButtonClick={() => + confirmDiscardIf(selectedContacts.length > 0, () => + setListIdToEdit(undefined) + ) + } onClose={handleClose} /> ); @@ -412,29 +443,31 @@ export const SendStoryModal = ({ }} page={page} onClose={handleClose} - onBackButtonClick={() => { - if (listIdToEdit) { - if ( - page === Page.AddViewer || - page === Page.HideStoryFrom || - page === Page.ChooseViewers - ) { - setPage(Page.EditingDistributionList); - } else { - setListIdToEdit(undefined); + onBackButtonClick={() => + confirmDiscardIf(selectedContacts.length > 0, () => { + if (listIdToEdit) { + if ( + page === Page.AddViewer || + page === Page.HideStoryFrom || + page === Page.ChooseViewers + ) { + setPage(Page.EditingDistributionList); + } else { + setListIdToEdit(undefined); + } + } else if (page === Page.HideStoryFrom || page === Page.AddViewer) { + setSelectedContacts([]); + setStagedMyStories(initialMyStories); + setStagedMyStoriesMemberUuids(initialMyStoriesMemberUuids); + setPage(Page.SetMyStoriesPrivacy); + } else if (page === Page.ChooseViewers) { + setSelectedContacts([]); + setPage(Page.SendStory); + } else if (page === Page.NameStory) { + setPage(Page.ChooseViewers); } - } else if (page === Page.HideStoryFrom || page === Page.AddViewer) { - setSelectedContacts([]); - setStagedMyStories(initialMyStories); - setStagedMyStoriesMemberUuids(initialMyStoriesMemberUuids); - setPage(Page.SetMyStoriesPrivacy); - } else if (page === Page.ChooseViewers) { - setSelectedContacts([]); - setPage(Page.SendStory); - } else if (page === Page.NameStory) { - setPage(Page.ChooseViewers); - } - }} + }) + } selectedContacts={selectedContacts} setSelectedContacts={setSelectedContacts} /> @@ -443,17 +476,19 @@ export const SendStoryModal = ({ const footer = ( <>
{selectedNames}
- + )}
{distributionLists.map(list => ( @@ -794,13 +863,15 @@ export const SendStoryModal = ({ return ( <> - - {modal} - + {!confirmDiscardModal && ( + confirmDiscardIf(selectedContacts.length > 0, onClose)} + > + {modal} + + )} {hasAnnouncementsOnlyAlert && ( )} + {confirmDiscardModal} ); }; diff --git a/ts/components/Stories.tsx b/ts/components/Stories.tsx index 8ba51b6e6..e9bb707ec 100644 --- a/ts/components/Stories.tsx +++ b/ts/components/Stories.tsx @@ -92,7 +92,8 @@ export const Stories = ({ useEscapeHandling( (isMyStories && myStories.length) || isViewingStory || - isStoriesSettingsVisible + isStoriesSettingsVisible || + addStoryData ? undefined : toggleStoriesView ); diff --git a/ts/components/StoriesSettingsModal.tsx b/ts/components/StoriesSettingsModal.tsx index e1fda7f25..73bd0af58 100644 --- a/ts/components/StoriesSettingsModal.tsx +++ b/ts/components/StoriesSettingsModal.tsx @@ -35,6 +35,7 @@ import { shouldNeverBeCalled, asyncShouldNeverBeCalled, } from '../util/shouldNeverBeCalled'; +import { useConfirmDiscard } from '../hooks/useConfirmDiscard'; export type PropsType = { candidateConversations: Array; @@ -103,6 +104,8 @@ export const StoriesSettingsModal = ({ setMyStoriesToAllSignalConnections, toggleSignalConnectionsModal, }: PropsType): JSX.Element => { + const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n); + const [listToEditId, setListToEditId] = useState( undefined ); @@ -144,17 +147,19 @@ export const StoriesSettingsModal = ({ onDistributionListCreated(name, uuids); resetChooseViewersScreen(); }} - onBackButtonClick={() => { - if (page === Page.HideStoryFrom) { - resetChooseViewersScreen(); - } else if (page === Page.NameStory) { - setPage(Page.ChooseViewers); - } else if (isChoosingViewers) { - resetChooseViewersScreen(); - } else if (listToEdit) { - setListToEditId(undefined); - } - }} + onBackButtonClick={() => + confirmDiscardIf(selectedContacts.length > 0, () => { + if (page === Page.HideStoryFrom) { + resetChooseViewersScreen(); + } else if (page === Page.NameStory) { + setPage(Page.ChooseViewers); + } else if (isChoosingViewers) { + resetChooseViewersScreen(); + } else if (listToEdit) { + setListToEditId(undefined); + } + }) + } onViewersUpdated={uuids => { if (listToEditId && page === Page.AddViewer) { onViewersUpdated(listToEditId, uuids); @@ -175,7 +180,7 @@ export const StoriesSettingsModal = ({ /> ); } else if (listToEdit) { - modal = onClose => ( + modal = handleClose => ( setListToEditId(undefined)} - onClose={onClose} + onClose={handleClose} /> ); } else { @@ -281,13 +286,17 @@ export const StoriesSettingsModal = ({ return ( <> - - {modal} - + {!confirmDiscardModal && ( + + confirmDiscardIf(selectedContacts.length > 0, hideStoriesSettings) + } + > + {modal} + + )} {confirmDeleteListId && ( )} + {confirmDiscardModal} ); }; @@ -396,7 +406,7 @@ export const DistributionListSettingsModal = ({ {isMyStories && ( { setPage(Page.HideStoryFrom); @@ -862,6 +872,7 @@ export const EditDistributionListModal = ({ }} value={searchTerm} /> + {selectedContacts.length ? ( {selectedContacts.map(contact => ( diff --git a/ts/components/StoryCreator.tsx b/ts/components/StoryCreator.tsx index ce9ce2335..9aaaed03e 100644 --- a/ts/components/StoryCreator.tsx +++ b/ts/components/StoryCreator.tsx @@ -143,6 +143,7 @@ export const StoryCreator = ({ <> {draftAttachment && ( { + const [showConfirmDiscardModal, setShowConfirmDiscardModal] = useState(false); + + const onTryClose = useCallback(() => { + setShowConfirmDiscardModal(true); + }, [setShowConfirmDiscardModal]); + const [isEditingText, setIsEditingText] = useState(false); const [selectedBackground, setSelectedBackground] = useState(BackgroundStyle.BG1); @@ -252,11 +259,11 @@ export const TextStoryCreator = ({ setIsColorPickerShowing(false); setIsEditingText(false); setIsLinkPreviewInputShowing(false); - event.preventDefault(); - event.stopPropagation(); } else { - onClose(); + onTryClose(); } + event.preventDefault(); + event.stopPropagation(); } }; @@ -271,7 +278,9 @@ export const TextStoryCreator = ({ isEditingText, isLinkPreviewInputShowing, colorPickerPopperButtonRef, - onClose, + showConfirmDiscardModal, + setShowConfirmDiscardModal, + onTryClose, ]); useEffect(() => { @@ -422,7 +431,7 @@ export const TextStoryCreator = ({ )}
+ {showConfirmDiscardModal && ( + setShowConfirmDiscardModal(false)} + onDiscard={onClose} + /> + )} ); diff --git a/ts/hooks/useConfirmDiscard.tsx b/ts/hooks/useConfirmDiscard.tsx new file mode 100644 index 000000000..862eb75b2 --- /dev/null +++ b/ts/hooks/useConfirmDiscard.tsx @@ -0,0 +1,33 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React, { useState } from 'react'; + +import type { PropsType } from '../components/ConfirmDiscardDialog'; + +import { ConfirmDiscardDialog } from '../components/ConfirmDiscardDialog'; +import type { LocalizerType } from '../types/Util'; + +export function useConfirmDiscard( + i18n: LocalizerType +): [JSX.Element | null, (condition: boolean, callback: () => void) => void] { + const [props, setProps] = useState | null>(null); + const confirmElement = props ? ( + + ) : null; + + function confirmDiscardIf(condition: boolean, callback: () => void) { + if (condition) { + setProps({ + onClose() { + setProps(null); + }, + onDiscard: callback, + }); + } else { + callback(); + } + } + + return [confirmElement, confirmDiscardIf]; +}