Added story preview and confirmation dialogs to send story

Co-authored-by: Alvaro <110414366+alvaro-signal@users.noreply.github.com>
This commit is contained in:
automated-signal 2022-10-12 09:34:22 -07:00 committed by GitHub
parent 3cf661bad4
commit 5570d6935b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 315 additions and 109 deletions

View file

@ -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 { &__item--contact-or-conversation {
height: 52px; height: 52px;
padding: 0 6px; padding: 0 6px;
@ -32,13 +45,17 @@
} }
&__new-story { &__new-story {
&__container { &__button {
font-weight: 500;
padding: 5px 10px;
&::before { &::before {
@include color-svg('../images/icons/v2/plus-20.svg', $color-white); @include color-svg('../images/icons/v2/plus-20.svg', $color-white);
content: ''; content: '';
height: 16px; height: 16px;
margin-right: 8px;
width: 16px; width: 16px;
margin-right: 8px;
display: inline-block;
vertical-align: text-bottom;
} }
} }
@ -121,11 +138,13 @@
&__name { &__name {
@include font-body-1-bold; @include font-body-1-bold;
font-weight: 400;
} }
&__description { &__description {
@include font-body-2; @include font-body-2;
color: $color-gray-60; font-size: 12px;
color: $color-gray-25;
} }
&__checkbox { &__checkbox {
@ -187,6 +206,9 @@
padding-right: 16px; padding-right: 16px;
user-select: none; user-select: none;
flex: 1; flex: 1;
display: flex;
align-items: center;
min-height: 32px;
} }
&__ok { &__ok {
@ -232,4 +254,15 @@
width: 18px; width: 18px;
} }
} }
// more specific to override StoryImage
.SendStoryModal {
&__story {
border-radius: 12px;
backdrop-filter: blur(90px);
&__image {
object-fit: contain;
}
}
}
} }

View file

@ -15,7 +15,7 @@ export const ConfirmDiscardDialog = ({
i18n, i18n,
onClose, onClose,
onDiscard, onDiscard,
}: PropsType): JSX.Element | null => { }: PropsType): JSX.Element => {
return ( return (
<ConfirmationDialog <ConfirmationDialog
dialogName="ConfirmDiscardDialog" dialogName="ConfirmDiscardDialog"

View file

@ -34,5 +34,5 @@ const getDefaultProps = (): PropsType<number> => ({
}); });
export const Default = (): JSX.Element => { export const Default = (): JSX.Element => {
return <ContextMenu {...getDefaultProps()} />; return <ContextMenu {...getDefaultProps()}>Menu</ContextMenu>;
}; };

View file

@ -23,20 +23,29 @@ export type ContextMenuOptionType<T> = {
readonly value?: T; readonly value?: T;
}; };
export type PropsType<T> = { type RenderButtonProps = {
readonly ariaLabel?: string; openMenu: (() => void) | ((ev: React.MouseEvent) => void);
readonly children?: ReactNode; onKeyDown: (ev: KeyboardEvent) => void;
readonly i18n: LocalizerType; isMenuShowing: boolean;
readonly menuOptions: ReadonlyArray<ContextMenuOptionType<T>>; ref: React.Ref<HTMLButtonElement> | null;
readonly moduleClassName?: string;
readonly onClick?: () => unknown;
readonly onMenuShowingChanged?: (value: boolean) => unknown;
readonly popperOptions?: Pick<Options, 'placement' | 'strategy'>;
readonly theme?: Theme;
readonly title?: string;
readonly value?: T;
}; };
export type PropsType<T> = 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<ContextMenuOptionType<T>>;
moduleClassName?: string;
button?: () => JSX.Element;
onClick?: () => unknown;
onMenuShowingChanged?: (value: boolean) => unknown;
popperOptions?: Pick<Options, 'placement' | 'strategy'>;
theme?: Theme;
title?: string;
value?: T;
}>;
let closeCurrentOpenContextMenu: undefined | (() => unknown); let closeCurrentOpenContextMenu: undefined | (() => unknown);
// https://popper.js.org/docs/v2/virtual-elements/ // https://popper.js.org/docs/v2/virtual-elements/
@ -167,13 +176,16 @@ export function ContextMenu<T>({
const getClassName = getClassNamesFor('ContextMenu', moduleClassName); const getClassName = getClassNamesFor('ContextMenu', moduleClassName);
return ( let buttonNode: ReactNode;
<div if (typeof children === 'function') {
className={classNames( buttonNode = (children as (props: RenderButtonProps) => JSX.Element)({
getClassName('__container'), openMenu: onClick || handleClick,
theme ? themeClassName(theme) : undefined onKeyDown: handleKeyDown,
)} isMenuShowing,
> ref: setReferenceElement,
});
} else {
buttonNode = (
<button <button
aria-label={ariaLabel || i18n('ContextMenu--button')} aria-label={ariaLabel || i18n('ContextMenu--button')}
className={classNames( className={classNames(
@ -188,6 +200,17 @@ export function ContextMenu<T>({
> >
{children} {children}
</button> </button>
);
}
return (
<div
className={classNames(
getClassName('__container'),
theme ? themeClassName(theme) : undefined
)}
>
{buttonNode}
{isMenuShowing && ( {isMenuShowing && (
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import Measure from 'react-measure'; import Measure from 'react-measure';
import React, { useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { fabric } from 'fabric'; import { fabric } from 'fabric';
@ -38,6 +38,7 @@ import { AddCaptionModal } from './AddCaptionModal';
import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea'; import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea';
import { Emojify } from './conversation/Emojify'; import { Emojify } from './conversation/Emojify';
import { AddNewLines } from './conversation/AddNewLines'; import { AddNewLines } from './conversation/AddNewLines';
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
export type PropsType = { export type PropsType = {
doneButtonLabel?: string; doneButtonLabel?: string;
@ -173,6 +174,12 @@ export const MediaEditor = ({
const [editMode, setEditMode] = useState<EditMode | undefined>(); const [editMode, setEditMode] = useState<EditMode | undefined>();
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n);
const onTryClose = useCallback(() => {
confirmDiscardIf(caption !== '' || Boolean(image), onClose);
}, [confirmDiscardIf, caption, image, onClose]);
// Keyboard support // Keyboard support
useEffect(() => { useEffect(() => {
if (!fabricCanvas) { if (!fabricCanvas) {
@ -207,7 +214,7 @@ export const MediaEditor = ({
// there's no easy way to prevent an ESC meant for the // there's no easy way to prevent an ESC meant for the
// sticker-picker from hitting this handler first // sticker-picker from hitting this handler first
if (!isStickerPopperOpen) { if (!isStickerPopperOpen) {
onClose(); onTryClose();
} }
} else { } else {
setEditMode(undefined); setEditMode(undefined);
@ -349,7 +356,7 @@ export const MediaEditor = ({
fabricCanvas, fabricCanvas,
editMode, editMode,
isStickerPopperOpen, isStickerPopperOpen,
onClose, onTryClose,
redoIfPossible, redoIfPossible,
undoIfPossible, undoIfPossible,
]); ]);
@ -957,7 +964,7 @@ export const MediaEditor = ({
)} )}
<div className="MediaEditor__toolbar--buttons"> <div className="MediaEditor__toolbar--buttons">
<Button <Button
onClick={onClose} onClick={onTryClose}
theme={Theme.Dark} theme={Theme.Dark}
variant={ButtonVariant.Secondary} variant={ButtonVariant.Secondary}
> >
@ -1142,7 +1149,7 @@ export const MediaEditor = ({
data = await canvasToBytes(renderedCanvas); data = await canvasToBytes(renderedCanvas);
} catch (err) { } catch (err) {
onClose(); onTryClose();
throw err; throw err;
} finally { } finally {
setIsSaving(false); setIsSaving(false);
@ -1157,6 +1164,7 @@ export const MediaEditor = ({
</Button> </Button>
</div> </div>
</div> </div>
{confirmDiscardModal}
</div>, </div>,
portal portal
); );

View file

@ -16,6 +16,7 @@ import {
getMyStories, getMyStories,
getFakeDistributionListsWithMembers, getFakeDistributionListsWithMembers,
} from '../test-both/helpers/getFakeDistributionLists'; } from '../test-both/helpers/getFakeDistributionLists';
import { VIDEO_MP4 } from '../types/MIME';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -28,6 +29,13 @@ export default {
title: 'Components/SendStoryModal', title: 'Components/SendStoryModal',
component: SendStoryModal, component: SendStoryModal,
argTypes: { argTypes: {
draftAttachment: {
defaultValue: {
contentType: VIDEO_MP4,
fileName: 'pixabay-Soap-Bubble-7141.mp4',
url: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
},
},
candidateConversations: { candidateConversations: {
defaultValue: Array.from(Array(100), () => getDefaultConversation()), defaultValue: Array.from(Array(100), () => getDefaultConversation()),
}, },

View file

@ -15,7 +15,7 @@ import type { StoryDistributionListWithMembersDataType } from '../types/Stories'
import type { UUIDStringType } from '../types/UUID'; import type { UUIDStringType } from '../types/UUID';
import { Alert } from './Alert'; import { Alert } from './Alert';
import { Avatar, AvatarSize } from './Avatar'; import { Avatar, AvatarSize } from './Avatar';
import { Button, ButtonVariant } from './Button'; import { Button, ButtonSize, ButtonVariant } from './Button';
import { Checkbox } from './Checkbox'; import { Checkbox } from './Checkbox';
import { ConfirmationDialog } from './ConfirmationDialog'; import { ConfirmationDialog } from './ConfirmationDialog';
import { ContextMenu } from './ContextMenu'; import { ContextMenu } from './ContextMenu';
@ -31,8 +31,14 @@ import { PagedModal, ModalPage } from './Modal';
import { StoryDistributionListName } from './StoryDistributionListName'; import { StoryDistributionListName } from './StoryDistributionListName';
import { Theme } from '../util/theme'; import { Theme } from '../util/theme';
import { isNotNil } from '../util/isNotNil'; 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 = { export type PropsType = {
draftAttachment: AttachmentType;
candidateConversations: Array<ConversationType>; candidateConversations: Array<ConversationType>;
distributionLists: Array<StoryDistributionListWithMembersDataType>; distributionLists: Array<StoryDistributionListWithMembersDataType>;
getPreferredBadge: PreferredBadgeSelectorType; getPreferredBadge: PreferredBadgeSelectorType;
@ -114,6 +120,7 @@ function getListViewers(
} }
export const SendStoryModal = ({ export const SendStoryModal = ({
draftAttachment,
candidateConversations, candidateConversations,
distributionLists, distributionLists,
getPreferredBadge, getPreferredBadge,
@ -138,6 +145,8 @@ export const SendStoryModal = ({
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [page, setPage] = useState<PageType>(Page.SendStory); const [page, setPage] = useState<PageType>(Page.SendStory);
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n);
const [selectedListIds, setSelectedListIds] = useState<Set<UUIDStringType>>( const [selectedListIds, setSelectedListIds] = useState<Set<UUIDStringType>>(
new Set() new Set()
); );
@ -267,6 +276,24 @@ export const SendStoryModal = ({
.join(', '); .join(', ');
} }
const [objectUrl, setObjectUrl] = useState<string | undefined>(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<ModalPropsType, 'hasXButton' | 'i18n'> = { const modalCommonProps: Pick<ModalPropsType, 'hasXButton' | 'i18n'> = {
hasXButton: true, hasXButton: true,
i18n, i18n,
@ -375,7 +402,11 @@ export const SendStoryModal = ({
setPage={setPage} setPage={setPage}
setSelectedContacts={setSelectedContacts} setSelectedContacts={setSelectedContacts}
toggleSignalConnectionsModal={toggleSignalConnectionsModal} toggleSignalConnectionsModal={toggleSignalConnectionsModal}
onBackButtonClick={() => setListIdToEdit(undefined)} onBackButtonClick={() =>
confirmDiscardIf(selectedContacts.length > 0, () =>
setListIdToEdit(undefined)
)
}
onClose={handleClose} onClose={handleClose}
/> />
); );
@ -412,29 +443,31 @@ export const SendStoryModal = ({
}} }}
page={page} page={page}
onClose={handleClose} onClose={handleClose}
onBackButtonClick={() => { onBackButtonClick={() =>
if (listIdToEdit) { confirmDiscardIf(selectedContacts.length > 0, () => {
if ( if (listIdToEdit) {
page === Page.AddViewer || if (
page === Page.HideStoryFrom || page === Page.AddViewer ||
page === Page.ChooseViewers page === Page.HideStoryFrom ||
) { page === Page.ChooseViewers
setPage(Page.EditingDistributionList); ) {
} else { setPage(Page.EditingDistributionList);
setListIdToEdit(undefined); } 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} selectedContacts={selectedContacts}
setSelectedContacts={setSelectedContacts} setSelectedContacts={setSelectedContacts}
/> />
@ -443,17 +476,19 @@ export const SendStoryModal = ({
const footer = ( const footer = (
<> <>
<div className="SendStoryModal__selected-lists">{selectedNames}</div> <div className="SendStoryModal__selected-lists">{selectedNames}</div>
<button {selectedNames.length > 0 && (
aria-label={i18n('ok')} <button
className="SendStoryModal__ok" aria-label={i18n('ok')}
disabled={!chosenGroupIds.size} className="SendStoryModal__ok"
onClick={() => { disabled={!chosenGroupIds.size}
toggleGroupsForStorySend(Array.from(chosenGroupIds)); onClick={() => {
setChosenGroupIds(new Set()); toggleGroupsForStorySend(Array.from(chosenGroupIds));
setPage(Page.SendStory); setChosenGroupIds(new Set());
}} setPage(Page.SendStory);
type="button" }}
/> type="button"
/>
)}
</> </>
); );
@ -461,6 +496,7 @@ export const SendStoryModal = ({
<ModalPage <ModalPage
modalName="SendStoryModal__choose-groups" modalName="SendStoryModal__choose-groups"
title={i18n('SendStoryModal__choose-groups')} title={i18n('SendStoryModal__choose-groups')}
moduleClassName="SendStoryModal"
modalFooter={footer} modalFooter={footer}
onClose={handleClose} onClose={handleClose}
{...modalCommonProps} {...modalCommonProps}
@ -548,17 +584,25 @@ export const SendStoryModal = ({
const footer = ( const footer = (
<> <>
<div className="SendStoryModal__selected-lists">{selectedNames}</div> <div className="SendStoryModal__selected-lists">{selectedNames}</div>
<button {selectedNames.length > 0 && (
aria-label={i18n('SendStoryModal__send')} <button
className="SendStoryModal__send" aria-label={i18n('SendStoryModal__send')}
disabled={!selectedListIds.size && !selectedGroupIds.size} className="SendStoryModal__send"
onClick={() => { disabled={!selectedListIds.size && !selectedGroupIds.size}
onSend(Array.from(selectedListIds), Array.from(selectedGroupIds)); onClick={() => {
}} onSend(Array.from(selectedListIds), Array.from(selectedGroupIds));
type="button" }}
/> type="button"
/>
)}
</> </>
); );
const attachment = {
...draftAttachment,
url: objectUrl,
};
modal = handleClose => ( modal = handleClose => (
<ModalPage <ModalPage
modalName="SendStoryModal__title" modalName="SendStoryModal__title"
@ -568,6 +612,20 @@ export const SendStoryModal = ({
onClose={handleClose} onClose={handleClose}
{...modalCommonProps} {...modalCommonProps}
> >
<div
className="SendStoryModal__story-preview"
style={{ backgroundImage: getStoryBackground(attachment) }}
>
<StoryImage
i18n={i18n}
firstName={i18n('you')}
queueStoryDownload={noop}
storyId="story-id"
label="label"
moduleClassName="SendStoryModal__story"
attachment={attachment}
/>
</div>
<div className="SendStoryModal__top-bar"> <div className="SendStoryModal__top-bar">
{i18n('stories')} {i18n('stories')}
<ContextMenu <ContextMenu
@ -594,7 +652,18 @@ export const SendStoryModal = ({
}} }}
theme={Theme.Dark} theme={Theme.Dark}
> >
{i18n('SendStoryModal__new')} {({ openMenu, onKeyDown, ref }) => (
<Button
ref={ref}
className="SendStoryModal__new-story__button"
variant={ButtonVariant.Secondary}
size={ButtonSize.Small}
onClick={openMenu}
onKeyDown={onKeyDown}
>
{i18n('SendStoryModal__new')}
</Button>
)}
</ContextMenu> </ContextMenu>
</div> </div>
{distributionLists.map(list => ( {distributionLists.map(list => (
@ -794,13 +863,15 @@ export const SendStoryModal = ({
return ( return (
<> <>
<PagedModal {!confirmDiscardModal && (
modalName="SendStoryModal" <PagedModal
theme={Theme.Dark} modalName="SendStoryModal"
onClose={onClose} theme={Theme.Dark}
> onClose={() => confirmDiscardIf(selectedContacts.length > 0, onClose)}
{modal} >
</PagedModal> {modal}
</PagedModal>
)}
{hasAnnouncementsOnlyAlert && ( {hasAnnouncementsOnlyAlert && (
<Alert <Alert
body={i18n('SendStoryModal__announcements-only')} body={i18n('SendStoryModal__announcements-only')}
@ -852,6 +923,7 @@ export const SendStoryModal = ({
{i18n('StoriesSettings__delete-list--confirm')} {i18n('StoriesSettings__delete-list--confirm')}
</ConfirmationDialog> </ConfirmationDialog>
)} )}
{confirmDiscardModal}
</> </>
); );
}; };

View file

@ -92,7 +92,8 @@ export const Stories = ({
useEscapeHandling( useEscapeHandling(
(isMyStories && myStories.length) || (isMyStories && myStories.length) ||
isViewingStory || isViewingStory ||
isStoriesSettingsVisible isStoriesSettingsVisible ||
addStoryData
? undefined ? undefined
: toggleStoriesView : toggleStoriesView
); );

View file

@ -35,6 +35,7 @@ import {
shouldNeverBeCalled, shouldNeverBeCalled,
asyncShouldNeverBeCalled, asyncShouldNeverBeCalled,
} from '../util/shouldNeverBeCalled'; } from '../util/shouldNeverBeCalled';
import { useConfirmDiscard } from '../hooks/useConfirmDiscard';
export type PropsType = { export type PropsType = {
candidateConversations: Array<ConversationType>; candidateConversations: Array<ConversationType>;
@ -103,6 +104,8 @@ export const StoriesSettingsModal = ({
setMyStoriesToAllSignalConnections, setMyStoriesToAllSignalConnections,
toggleSignalConnectionsModal, toggleSignalConnectionsModal,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [confirmDiscardModal, confirmDiscardIf] = useConfirmDiscard(i18n);
const [listToEditId, setListToEditId] = useState<string | undefined>( const [listToEditId, setListToEditId] = useState<string | undefined>(
undefined undefined
); );
@ -144,17 +147,19 @@ export const StoriesSettingsModal = ({
onDistributionListCreated(name, uuids); onDistributionListCreated(name, uuids);
resetChooseViewersScreen(); resetChooseViewersScreen();
}} }}
onBackButtonClick={() => { onBackButtonClick={() =>
if (page === Page.HideStoryFrom) { confirmDiscardIf(selectedContacts.length > 0, () => {
resetChooseViewersScreen(); if (page === Page.HideStoryFrom) {
} else if (page === Page.NameStory) { resetChooseViewersScreen();
setPage(Page.ChooseViewers); } else if (page === Page.NameStory) {
} else if (isChoosingViewers) { setPage(Page.ChooseViewers);
resetChooseViewersScreen(); } else if (isChoosingViewers) {
} else if (listToEdit) { resetChooseViewersScreen();
setListToEditId(undefined); } else if (listToEdit) {
} setListToEditId(undefined);
}} }
})
}
onViewersUpdated={uuids => { onViewersUpdated={uuids => {
if (listToEditId && page === Page.AddViewer) { if (listToEditId && page === Page.AddViewer) {
onViewersUpdated(listToEditId, uuids); onViewersUpdated(listToEditId, uuids);
@ -175,7 +180,7 @@ export const StoriesSettingsModal = ({
/> />
); );
} else if (listToEdit) { } else if (listToEdit) {
modal = onClose => ( modal = handleClose => (
<DistributionListSettingsModal <DistributionListSettingsModal
key="settings-modal" key="settings-modal"
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}
@ -189,7 +194,7 @@ export const StoriesSettingsModal = ({
setSelectedContacts={setSelectedContacts} setSelectedContacts={setSelectedContacts}
toggleSignalConnectionsModal={toggleSignalConnectionsModal} toggleSignalConnectionsModal={toggleSignalConnectionsModal}
onBackButtonClick={() => setListToEditId(undefined)} onBackButtonClick={() => setListToEditId(undefined)}
onClose={onClose} onClose={handleClose}
/> />
); );
} else { } else {
@ -281,13 +286,17 @@ export const StoriesSettingsModal = ({
return ( return (
<> <>
<PagedModal {!confirmDiscardModal && (
modalName="StoriesSettingsModal" <PagedModal
theme={Theme.Dark} modalName="StoriesSettingsModal"
onClose={hideStoriesSettings} theme={Theme.Dark}
> onClose={() =>
{modal} confirmDiscardIf(selectedContacts.length > 0, hideStoriesSettings)
</PagedModal> }
>
{modal}
</PagedModal>
)}
{confirmDeleteListId && ( {confirmDeleteListId && (
<ConfirmationDialog <ConfirmationDialog
dialogName="StoriesSettings.deleteList" dialogName="StoriesSettings.deleteList"
@ -309,6 +318,7 @@ export const StoriesSettingsModal = ({
{i18n('StoriesSettings__delete-list--confirm')} {i18n('StoriesSettings__delete-list--confirm')}
</ConfirmationDialog> </ConfirmationDialog>
)} )}
{confirmDiscardModal}
</> </>
); );
}; };
@ -396,7 +406,7 @@ export const DistributionListSettingsModal = ({
{isMyStories && ( {isMyStories && (
<EditMyStoriesPrivacy <EditMyStoriesPrivacy
i18n={i18n} i18n={i18n}
learnMore="StoriesSettings__mine_disclaimer" learnMore="StoriesSettings__mine__disclaimer"
myStories={listToEdit} myStories={listToEdit}
onClickExclude={() => { onClickExclude={() => {
setPage(Page.HideStoryFrom); setPage(Page.HideStoryFrom);
@ -862,6 +872,7 @@ export const EditDistributionListModal = ({
}} }}
value={searchTerm} value={searchTerm}
/> />
{selectedContacts.length ? ( {selectedContacts.length ? (
<ContactPills moduleClassName="StoriesSettingsModal__tags"> <ContactPills moduleClassName="StoriesSettingsModal__tags">
{selectedContacts.map(contact => ( {selectedContacts.map(contact => (

View file

@ -143,6 +143,7 @@ export const StoryCreator = ({
<> <>
{draftAttachment && ( {draftAttachment && (
<SendStoryModal <SendStoryModal
draftAttachment={draftAttachment}
candidateConversations={candidateConversations} candidateConversations={candidateConversations}
distributionLists={distributionLists} distributionLists={distributionLists}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import React, { useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { get, has, noop } from 'lodash'; import { get, has, noop } from 'lodash';
import { usePopper } from 'react-popper'; import { usePopper } from 'react-popper';
@ -28,6 +28,7 @@ import {
} from '../util/getStoryBackground'; } from '../util/getStoryBackground';
import { objectMap } from '../util/objectMap'; import { objectMap } from '../util/objectMap';
import { handleOutsideClick } from '../util/handleOutsideClick'; import { handleOutsideClick } from '../util/handleOutsideClick';
import { ConfirmDiscardDialog } from './ConfirmDiscardDialog';
export type PropsType = { export type PropsType = {
debouncedMaybeGrabLinkPreview: ( debouncedMaybeGrabLinkPreview: (
@ -125,6 +126,12 @@ export const TextStoryCreator = ({
onClose, onClose,
onDone, onDone,
}: PropsType): JSX.Element => { }: PropsType): JSX.Element => {
const [showConfirmDiscardModal, setShowConfirmDiscardModal] = useState(false);
const onTryClose = useCallback(() => {
setShowConfirmDiscardModal(true);
}, [setShowConfirmDiscardModal]);
const [isEditingText, setIsEditingText] = useState(false); const [isEditingText, setIsEditingText] = useState(false);
const [selectedBackground, setSelectedBackground] = const [selectedBackground, setSelectedBackground] =
useState<BackgroundStyleType>(BackgroundStyle.BG1); useState<BackgroundStyleType>(BackgroundStyle.BG1);
@ -252,11 +259,11 @@ export const TextStoryCreator = ({
setIsColorPickerShowing(false); setIsColorPickerShowing(false);
setIsEditingText(false); setIsEditingText(false);
setIsLinkPreviewInputShowing(false); setIsLinkPreviewInputShowing(false);
event.preventDefault();
event.stopPropagation();
} else { } else {
onClose(); onTryClose();
} }
event.preventDefault();
event.stopPropagation();
} }
}; };
@ -271,7 +278,9 @@ export const TextStoryCreator = ({
isEditingText, isEditingText,
isLinkPreviewInputShowing, isLinkPreviewInputShowing,
colorPickerPopperButtonRef, colorPickerPopperButtonRef,
onClose, showConfirmDiscardModal,
setShowConfirmDiscardModal,
onTryClose,
]); ]);
useEffect(() => { useEffect(() => {
@ -422,7 +431,7 @@ export const TextStoryCreator = ({
)} )}
<div className="StoryCreator__toolbar--buttons"> <div className="StoryCreator__toolbar--buttons">
<Button <Button
onClick={onClose} onClick={onTryClose}
theme={Theme.Dark} theme={Theme.Dark}
variant={ButtonVariant.Secondary} variant={ButtonVariant.Secondary}
> >
@ -566,6 +575,13 @@ export const TextStoryCreator = ({
</Button> </Button>
</div> </div>
</div> </div>
{showConfirmDiscardModal && (
<ConfirmDiscardDialog
i18n={i18n}
onClose={() => setShowConfirmDiscardModal(false)}
onDiscard={onClose}
/>
)}
</div> </div>
</FocusTrap> </FocusTrap>
); );

View file

@ -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<Omit<PropsType, 'i18n'> | null>(null);
const confirmElement = props ? (
<ConfirmDiscardDialog i18n={i18n} {...props} />
) : null;
function confirmDiscardIf(condition: boolean, callback: () => void) {
if (condition) {
setProps({
onClose() {
setProps(null);
},
onDiscard: callback,
});
} else {
callback();
}
}
return [confirmElement, confirmDiscardIf];
}