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:
parent
3cf661bad4
commit
5570d6935b
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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={{
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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()),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -92,7 +92,8 @@ export const Stories = ({
|
||||||
useEscapeHandling(
|
useEscapeHandling(
|
||||||
(isMyStories && myStories.length) ||
|
(isMyStories && myStories.length) ||
|
||||||
isViewingStory ||
|
isViewingStory ||
|
||||||
isStoriesSettingsVisible
|
isStoriesSettingsVisible ||
|
||||||
|
addStoryData
|
||||||
? undefined
|
? undefined
|
||||||
: toggleStoriesView
|
: toggleStoriesView
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 => (
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
33
ts/hooks/useConfirmDiscard.tsx
Normal file
33
ts/hooks/useConfirmDiscard.tsx
Normal 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];
|
||||||
|
}
|
Loading…
Reference in a new issue