Edit distribution lists via story settings menu
This commit is contained in:
parent
9986d10947
commit
e321e1fea8
42 changed files with 2403 additions and 102 deletions
|
@ -15,6 +15,7 @@ export type PropsType = {
|
|||
moduleClassName?: string;
|
||||
name: string;
|
||||
onChange: (value: boolean) => unknown;
|
||||
onClick?: () => unknown;
|
||||
};
|
||||
|
||||
export const Checkbox = ({
|
||||
|
@ -26,6 +27,7 @@ export const Checkbox = ({
|
|||
moduleClassName,
|
||||
name,
|
||||
onChange,
|
||||
onClick,
|
||||
}: PropsType): JSX.Element => {
|
||||
const getClassName = getClassNamesFor('Checkbox', moduleClassName);
|
||||
const id = useMemo(() => `${name}::${uuid()}`, [name]);
|
||||
|
@ -39,12 +41,15 @@ export const Checkbox = ({
|
|||
id={id}
|
||||
name={name}
|
||||
onChange={ev => onChange(ev.target.checked)}
|
||||
onClick={onClick}
|
||||
type={isRadio ? 'radio' : 'checkbox'}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor={id}>{label}</label>
|
||||
<div className={getClassName('__description')}>{description}</div>
|
||||
<label htmlFor={id}>
|
||||
<div>{label}</div>
|
||||
<div className={getClassName('__description')}>{description}</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -93,7 +93,9 @@ export function ContextMenuPopper<T>({
|
|||
>
|
||||
<div className={theme ? themeClassName(theme) : undefined}>
|
||||
<div
|
||||
className="ContextMenu__popper"
|
||||
className={classNames('ContextMenu__popper', {
|
||||
'ContextMenu__popper--single-item': menuOptions.length === 1,
|
||||
})}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
|
|
|
@ -34,9 +34,12 @@ import type { BodyRangeType, LocalizerType, ThemeType } from '../types/Util';
|
|||
import { ModalHost } from './ModalHost';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
||||
import { assert } from '../util/assert';
|
||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
||||
import { useAnimated } from '../hooks/useAnimated';
|
||||
import {
|
||||
shouldNeverBeCalled,
|
||||
asyncShouldNeverBeCalled,
|
||||
} from '../util/shouldNeverBeCalled';
|
||||
|
||||
export type DataPropsType = {
|
||||
attachments?: Array<AttachmentType>;
|
||||
|
@ -470,15 +473,3 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function shouldNeverBeCalled(..._args: ReadonlyArray<unknown>): void {
|
||||
assert(false, 'This should never be called. Doing nothing');
|
||||
}
|
||||
|
||||
async function asyncShouldNeverBeCalled(
|
||||
..._args: ReadonlyArray<unknown>
|
||||
): Promise<undefined> {
|
||||
shouldNeverBeCalled();
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { missingCaseError } from '../util/missingCaseError';
|
|||
|
||||
import { ButtonVariant } from './Button';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { SignalConnectionsModal } from './SignalConnectionsModal';
|
||||
import { WhatsNewModal } from './WhatsNewModal';
|
||||
|
||||
type PropsType = {
|
||||
|
@ -28,6 +29,12 @@ type PropsType = {
|
|||
// SafetyNumberModal
|
||||
safetyNumberModalContactId?: string;
|
||||
renderSafetyNumber: () => JSX.Element;
|
||||
// SignalConnectionsModal
|
||||
isSignalConnectionsVisible: boolean;
|
||||
toggleSignalConnectionsModal: () => unknown;
|
||||
// StoriesSettings
|
||||
isStoriesSettingsVisible: boolean;
|
||||
renderStoriesSettings: () => JSX.Element;
|
||||
// UserNotFoundModal
|
||||
hideUserNotFoundModal: () => unknown;
|
||||
userNotFoundModalState?: UserNotFoundModalStateType;
|
||||
|
@ -50,6 +57,12 @@ export const GlobalModalContainer = ({
|
|||
// SafetyNumberModal
|
||||
safetyNumberModalContactId,
|
||||
renderSafetyNumber,
|
||||
// SignalConnectionsModal
|
||||
isSignalConnectionsVisible,
|
||||
toggleSignalConnectionsModal,
|
||||
// StoriesSettings
|
||||
isStoriesSettingsVisible,
|
||||
renderStoriesSettings,
|
||||
// UserNotFoundModal
|
||||
hideUserNotFoundModal,
|
||||
userNotFoundModalState,
|
||||
|
@ -105,5 +118,18 @@ export const GlobalModalContainer = ({
|
|||
return renderForwardMessageModal();
|
||||
}
|
||||
|
||||
if (isSignalConnectionsVisible) {
|
||||
return (
|
||||
<SignalConnectionsModal
|
||||
i18n={i18n}
|
||||
onClose={toggleSignalConnectionsModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isStoriesSettingsVisible) {
|
||||
return renderStoriesSettings();
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -23,7 +23,9 @@ const LOREM_IPSUM =
|
|||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor. Pellentesque auctor nisi id magna consequat sagittis. Curabitur dapibus enim sit amet elit pharetra tincidunt feugiat nisl imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros. Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.';
|
||||
|
||||
export const BareBonesShort = (): JSX.Element => (
|
||||
<Modal i18n={i18n}>Hello world!</Modal>
|
||||
<Modal i18n={i18n} useFocusTrap={false}>
|
||||
Hello world!
|
||||
</Modal>
|
||||
);
|
||||
|
||||
BareBonesShort.story = {
|
||||
|
@ -31,7 +33,7 @@ BareBonesShort.story = {
|
|||
};
|
||||
|
||||
export const BareBonesLong = (): JSX.Element => (
|
||||
<Modal i18n={i18n}>
|
||||
<Modal i18n={i18n} useFocusTrap={false}>
|
||||
<p>{LOREM_IPSUM}</p>
|
||||
<p>{LOREM_IPSUM}</p>
|
||||
<p>{LOREM_IPSUM}</p>
|
||||
|
@ -96,7 +98,7 @@ LotsOfButtonsInTheFooter.story = {
|
|||
};
|
||||
|
||||
export const LongBodyWithTitle = (): JSX.Element => (
|
||||
<Modal i18n={i18n} title="Hello world" onClose={onClose}>
|
||||
<Modal i18n={i18n} title="Hello world" onClose={onClose} useFocusTrap={false}>
|
||||
<p>{LOREM_IPSUM}</p>
|
||||
<p>{LOREM_IPSUM}</p>
|
||||
<p>{LOREM_IPSUM}</p>
|
||||
|
@ -195,3 +197,19 @@ export const StickyFooterLotsOfButtons = (): JSX.Element => (
|
|||
StickyFooterLotsOfButtons.story = {
|
||||
name: 'Sticky footer, Lots of buttons',
|
||||
};
|
||||
|
||||
export const WithBackButton = (): JSX.Element => (
|
||||
<Modal
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onBackButtonClick={noop}
|
||||
useFocusTrap={false}
|
||||
title="The Modal Title"
|
||||
>
|
||||
Hello world!
|
||||
</Modal>
|
||||
);
|
||||
|
||||
WithBackButton.story = {
|
||||
name: 'Back Button',
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ type PropsType = {
|
|||
hasXButton?: boolean;
|
||||
i18n: LocalizerType;
|
||||
moduleClassName?: string;
|
||||
onBackButtonClick?: () => unknown;
|
||||
onClose?: () => void;
|
||||
title?: ReactNode;
|
||||
useFocusTrap?: boolean;
|
||||
|
@ -42,6 +43,7 @@ export function Modal({
|
|||
i18n,
|
||||
moduleClassName,
|
||||
noMouseClose,
|
||||
onBackButtonClick,
|
||||
onClose = noop,
|
||||
title,
|
||||
theme,
|
||||
|
@ -70,6 +72,7 @@ export function Modal({
|
|||
hasXButton={hasXButton}
|
||||
i18n={i18n}
|
||||
moduleClassName={moduleClassName}
|
||||
onBackButtonClick={onBackButtonClick}
|
||||
onClose={close}
|
||||
title={title}
|
||||
>
|
||||
|
@ -86,6 +89,7 @@ export function ModalWindow({
|
|||
hasXButton,
|
||||
i18n,
|
||||
moduleClassName,
|
||||
onBackButtonClick,
|
||||
onClose = noop,
|
||||
title,
|
||||
}: Readonly<PropsType>): JSX.Element {
|
||||
|
@ -97,7 +101,7 @@ export function ModalWindow({
|
|||
const [scrolled, setScrolled] = useState(false);
|
||||
const [hasOverflow, setHasOverflow] = useState(false);
|
||||
|
||||
const hasHeader = Boolean(hasXButton || title);
|
||||
const hasHeader = Boolean(hasXButton || title || onBackButtonClick);
|
||||
const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
|
||||
|
||||
function handleResize({ scroll }: ContentRect) {
|
||||
|
@ -127,14 +131,21 @@ export function ModalWindow({
|
|||
}}
|
||||
>
|
||||
{hasHeader && (
|
||||
<div className={getClassName('__header')}>
|
||||
{hasXButton && (
|
||||
<div
|
||||
className={classNames(
|
||||
getClassName('__header'),
|
||||
onBackButtonClick
|
||||
? getClassName('__header--with-back-button')
|
||||
: null
|
||||
)}
|
||||
>
|
||||
{onBackButtonClick && (
|
||||
<button
|
||||
aria-label={i18n('close')}
|
||||
type="button"
|
||||
className={getClassName('__close-button')}
|
||||
aria-label={i18n('back')}
|
||||
className={getClassName('__back-button')}
|
||||
onClick={onBackButtonClick}
|
||||
tabIndex={0}
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
{title && (
|
||||
|
@ -147,6 +158,18 @@ export function ModalWindow({
|
|||
{title}
|
||||
</h1>
|
||||
)}
|
||||
{hasXButton && !title && (
|
||||
<div className={getClassName('__title')} />
|
||||
)}
|
||||
{hasXButton && (
|
||||
<button
|
||||
aria-label={i18n('close')}
|
||||
className={getClassName('__close-button')}
|
||||
onClick={onClose}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Measure scroll onResize={handleResize}>
|
||||
|
|
|
@ -7,8 +7,9 @@ import type { LocalizerType } from '../types/Util';
|
|||
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
import { MY_STORIES_ID, StoryViewModeType } from '../types/Stories';
|
||||
import { StoryViewModeType } from '../types/Stories';
|
||||
import { MessageTimestamp } from './conversation/MessageTimestamp';
|
||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||
import { StoryImage } from './StoryImage';
|
||||
import { Theme } from '../util/theme';
|
||||
|
||||
|
@ -69,9 +70,11 @@ export const MyStories = ({
|
|||
{myStories.map(list => (
|
||||
<div className="MyStories__distribution" key={list.distributionId}>
|
||||
<div className="MyStories__distribution__title">
|
||||
{list.distributionId === MY_STORIES_ID
|
||||
? i18n('Stories__mine')
|
||||
: list.distributionName}
|
||||
<StoryDistributionListName
|
||||
i18n={i18n}
|
||||
id={list.distributionId}
|
||||
name={list.distributionName}
|
||||
/>
|
||||
</div>
|
||||
{list.stories.map(story => (
|
||||
<div className="MyStories__story" key={story.timestamp}>
|
||||
|
|
28
ts/components/SignalConnectionsModal.stories.tsx
Normal file
28
ts/components/SignalConnectionsModal.stories.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Meta, Story } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { PropsType } from './SignalConnectionsModal';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { SignalConnectionsModal } from './SignalConnectionsModal';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/SignalConnectionsModal',
|
||||
component: SignalConnectionsModal,
|
||||
argTypes: {
|
||||
i18n: {
|
||||
defaultValue: i18n,
|
||||
},
|
||||
onClose: { action: true },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<PropsType> = args => <SignalConnectionsModal {...args} />;
|
||||
|
||||
export const Modal = Template.bind({});
|
||||
Modal.args = {};
|
55
ts/components/SignalConnectionsModal.tsx
Normal file
55
ts/components/SignalConnectionsModal.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Intl } from './Intl';
|
||||
import { Modal } from './Modal';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export const SignalConnectionsModal = ({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element => {
|
||||
return (
|
||||
<Modal hasXButton i18n={i18n} onClose={onClose}>
|
||||
<div className="SignalConnectionsModal">
|
||||
<i className="SignalConnectionsModal__icon" />
|
||||
|
||||
<div className="SignalConnectionsModal__description">
|
||||
<Intl
|
||||
components={{
|
||||
connections: (
|
||||
<strong>{i18n('SignalConnectionsModal__title')}</strong>
|
||||
),
|
||||
}}
|
||||
i18n={i18n}
|
||||
id="SignalConnectionsModal__header"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul className="SignalConnectionsModal__list">
|
||||
<li>{i18n('SignalConnectionsModal__bullet--1')}</li>
|
||||
<li>{i18n('SignalConnectionsModal__bullet--2')}</li>
|
||||
<li>{i18n('SignalConnectionsModal__bullet--3')}</li>
|
||||
</ul>
|
||||
|
||||
<div className="SignalConnectionsModal__description">
|
||||
{i18n('SignalConnectionsModal__footer')}
|
||||
</div>
|
||||
|
||||
<div className="SignalConnectionsModal__button">
|
||||
<Button onClick={onClose} variant={ButtonVariant.Primary}>
|
||||
{i18n('Confirmation--confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -46,6 +46,7 @@ export default {
|
|||
renderStoryCreator: { action: true },
|
||||
renderStoryViewer: { action: true },
|
||||
showConversation: { action: true },
|
||||
showStoriesSettings: { action: true },
|
||||
stories: {
|
||||
defaultValue: [],
|
||||
},
|
||||
|
|
|
@ -33,6 +33,7 @@ export type PropsType = {
|
|||
queueStoryDownload: (storyId: string) => unknown;
|
||||
renderStoryCreator: (props: SmartStoryCreatorPropsType) => JSX.Element;
|
||||
showConversation: ShowConversationType;
|
||||
showStoriesSettings: () => unknown;
|
||||
stories: Array<ConversationStoryType>;
|
||||
toggleHideStories: (conversationId: string) => unknown;
|
||||
toggleStoriesView: () => unknown;
|
||||
|
@ -52,6 +53,7 @@ export const Stories = ({
|
|||
queueStoryDownload,
|
||||
renderStoryCreator,
|
||||
showConversation,
|
||||
showStoriesSettings,
|
||||
stories,
|
||||
toggleHideStories,
|
||||
toggleStoriesView,
|
||||
|
@ -98,6 +100,7 @@ export const Stories = ({
|
|||
setIsShowingStoryCreator(true);
|
||||
}
|
||||
}}
|
||||
onStoriesSettings={showStoriesSettings}
|
||||
onStoryClicked={viewUserStories}
|
||||
queueStoryDownload={queueStoryDownload}
|
||||
showConversation={showConversation}
|
||||
|
|
|
@ -15,9 +15,11 @@ import type {
|
|||
StoryViewType,
|
||||
} from '../types/Stories';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
import { MyStoriesButton } from './MyStoriesButton';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { StoryListItem } from './StoryListItem';
|
||||
import { Theme } from '../util/theme';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
|
||||
const FUSE_OPTIONS: Fuse.IFuseOptions<ConversationStoryType> = {
|
||||
|
@ -63,6 +65,7 @@ export type PropsType = {
|
|||
myStories: Array<MyStoryType>;
|
||||
onAddStory: () => unknown;
|
||||
onMyStoriesClicked: () => unknown;
|
||||
onStoriesSettings: () => unknown;
|
||||
onStoryClicked: (conversationId: string) => unknown;
|
||||
queueStoryDownload: (storyId: string) => unknown;
|
||||
showConversation: ShowConversationType;
|
||||
|
@ -78,6 +81,7 @@ export const StoriesPane = ({
|
|||
myStories,
|
||||
onAddStory,
|
||||
onMyStoriesClicked,
|
||||
onStoriesSettings,
|
||||
onStoryClicked,
|
||||
queueStoryDownload,
|
||||
showConversation,
|
||||
|
@ -117,6 +121,21 @@ export const StoriesPane = ({
|
|||
onClick={onAddStory}
|
||||
type="button"
|
||||
/>
|
||||
<ContextMenu
|
||||
buttonClassName="Stories__pane__settings"
|
||||
i18n={i18n}
|
||||
menuOptions={[
|
||||
{
|
||||
onClick: () => onStoriesSettings(),
|
||||
label: i18n('StoriesSettings__context-menu'),
|
||||
},
|
||||
]}
|
||||
popperOptions={{
|
||||
placement: 'bottom',
|
||||
strategy: 'absolute',
|
||||
}}
|
||||
theme={Theme.Dark}
|
||||
/>
|
||||
</div>
|
||||
<SearchInput
|
||||
i18n={i18n}
|
||||
|
|
105
ts/components/StoriesSettingsModal.stories.tsx
Normal file
105
ts/components/StoriesSettingsModal.stories.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Meta, Story } from '@storybook/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { PropsType } from './StoriesSettingsModal';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { MY_STORIES_ID } from '../types/Stories';
|
||||
import { StoriesSettingsModal } from './StoriesSettingsModal';
|
||||
import { UUID } from '../types/UUID';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
export default {
|
||||
title: 'Components/StoriesSettingsModal',
|
||||
component: StoriesSettingsModal,
|
||||
argTypes: {
|
||||
candidateConversations: {
|
||||
defaultValue: Array.from(Array(100), () => getDefaultConversation()),
|
||||
},
|
||||
distributionLists: {
|
||||
defaultValue: [],
|
||||
},
|
||||
getPreferredBadge: { action: true },
|
||||
hideStoriesSettings: { action: true },
|
||||
i18n: {
|
||||
defaultValue: i18n,
|
||||
},
|
||||
me: {
|
||||
defaultValue: getDefaultConversation(),
|
||||
},
|
||||
onDeleteList: { action: true },
|
||||
onDistributionListCreated: { action: true },
|
||||
onHideMyStoriesFrom: { action: true },
|
||||
onRemoveMember: { action: true },
|
||||
onRepliesNReactionsChanged: { action: true },
|
||||
onViewersUpdated: { action: true },
|
||||
setMyStoriesToAllSignalConnections: { action: true },
|
||||
toggleSignalConnectionsModal: { action: true },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<PropsType> = args => <StoriesSettingsModal {...args} />;
|
||||
|
||||
export const MyStories = Template.bind({});
|
||||
MyStories.args = {
|
||||
distributionLists: [
|
||||
{
|
||||
allowsReplies: true,
|
||||
id: MY_STORIES_ID,
|
||||
isBlockList: false,
|
||||
members: [],
|
||||
name: MY_STORIES_ID,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const MyStoriesBlockList = Template.bind({});
|
||||
MyStoriesBlockList.args = {
|
||||
distributionLists: [
|
||||
{
|
||||
allowsReplies: true,
|
||||
id: MY_STORIES_ID,
|
||||
isBlockList: true,
|
||||
members: Array.from(Array(2), () => getDefaultConversation()),
|
||||
name: MY_STORIES_ID,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const MyStoriesExclusive = Template.bind({});
|
||||
MyStoriesExclusive.args = {
|
||||
distributionLists: [
|
||||
{
|
||||
allowsReplies: false,
|
||||
id: MY_STORIES_ID,
|
||||
isBlockList: false,
|
||||
members: Array.from(Array(11), () => getDefaultConversation()),
|
||||
name: MY_STORIES_ID,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const SingleList = Template.bind({});
|
||||
SingleList.args = {
|
||||
distributionLists: [
|
||||
{
|
||||
allowsReplies: true,
|
||||
id: MY_STORIES_ID,
|
||||
isBlockList: false,
|
||||
members: [],
|
||||
name: MY_STORIES_ID,
|
||||
},
|
||||
{
|
||||
allowsReplies: true,
|
||||
id: UUID.generate().toString(),
|
||||
isBlockList: false,
|
||||
members: Array.from(Array(4), () => getDefaultConversation()),
|
||||
name: 'Thailand 2021',
|
||||
},
|
||||
],
|
||||
};
|
766
ts/components/StoriesSettingsModal.tsx
Normal file
766
ts/components/StoriesSettingsModal.tsx
Normal file
|
@ -0,0 +1,766 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { MeasuredComponentProps } from 'react-measure';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||
import type { Row } from './ConversationList';
|
||||
import type { StoryDistributionListWithMembersDataType } from '../types/Stories';
|
||||
import type { UUIDStringType } from '../types/UUID';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { Checkbox } from './Checkbox';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { ConversationList, RowType } from './ConversationList';
|
||||
import { Input } from './Input';
|
||||
import { Intl } from './Intl';
|
||||
import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories';
|
||||
import { Modal } from './Modal';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||
import { Theme } from '../util/theme';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { UUID } from '../types/UUID';
|
||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import {
|
||||
shouldNeverBeCalled,
|
||||
asyncShouldNeverBeCalled,
|
||||
} from '../util/shouldNeverBeCalled';
|
||||
|
||||
export type PropsType = {
|
||||
candidateConversations: Array<ConversationType>;
|
||||
distributionLists: Array<StoryDistributionListWithMembersDataType>;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
hideStoriesSettings: () => unknown;
|
||||
i18n: LocalizerType;
|
||||
me: ConversationType;
|
||||
onDeleteList: (listId: string) => unknown;
|
||||
onDistributionListCreated: (
|
||||
name: string,
|
||||
viewerUuids: Array<UUIDStringType>
|
||||
) => unknown;
|
||||
onHideMyStoriesFrom: (viewerUuids: Array<UUIDStringType>) => unknown;
|
||||
onRemoveMember: (listId: string, uuid: UUIDStringType | undefined) => unknown;
|
||||
onRepliesNReactionsChanged: (
|
||||
listId: string,
|
||||
allowsReplies: boolean
|
||||
) => unknown;
|
||||
onViewersUpdated: (
|
||||
listId: string,
|
||||
viewerUuids: Array<UUIDStringType>
|
||||
) => unknown;
|
||||
setMyStoriesToAllSignalConnections: () => unknown;
|
||||
toggleSignalConnectionsModal: () => unknown;
|
||||
};
|
||||
|
||||
enum Page {
|
||||
DistributionLists = 'DistributionLists',
|
||||
AddViewer = 'AddViewer',
|
||||
ChooseViewers = 'ChooseViewers',
|
||||
NameStory = 'NameStory',
|
||||
HideStoryFrom = 'HideStoryFrom',
|
||||
}
|
||||
|
||||
export const StoriesSettingsModal = ({
|
||||
candidateConversations,
|
||||
distributionLists,
|
||||
getPreferredBadge,
|
||||
hideStoriesSettings,
|
||||
i18n,
|
||||
me,
|
||||
onDeleteList,
|
||||
onDistributionListCreated,
|
||||
onHideMyStoriesFrom,
|
||||
onRemoveMember,
|
||||
onRepliesNReactionsChanged,
|
||||
onViewersUpdated,
|
||||
setMyStoriesToAllSignalConnections,
|
||||
toggleSignalConnectionsModal,
|
||||
}: PropsType): JSX.Element => {
|
||||
const [listToEditId, setListToEditId] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const listToEdit = useMemo(
|
||||
() => distributionLists.find(x => x.id === listToEditId),
|
||||
[distributionLists, listToEditId]
|
||||
);
|
||||
|
||||
const [page, setPage] = useState<Page>(Page.DistributionLists);
|
||||
|
||||
const [storyName, setStoryName] = useState('');
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const [filteredConversations, setFilteredConversations] = useState(
|
||||
filterAndSortConversationsByRecent(
|
||||
candidateConversations,
|
||||
searchTerm,
|
||||
undefined
|
||||
)
|
||||
);
|
||||
|
||||
const [selectedContacts, setSelectedContacts] = useState<
|
||||
Array<ConversationType>
|
||||
>([]);
|
||||
|
||||
const contactLookup = useMemo(() => {
|
||||
const map = new Map();
|
||||
candidateConversations.forEach(contact => {
|
||||
map.set(contact.id, contact);
|
||||
});
|
||||
return map;
|
||||
}, [candidateConversations]);
|
||||
|
||||
const toggleSelectedConversation = useCallback(
|
||||
(conversationId: string) => {
|
||||
let removeContact = false;
|
||||
const nextSelectedContacts = selectedContacts.filter(contact => {
|
||||
if (contact.id === conversationId) {
|
||||
removeContact = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (removeContact) {
|
||||
setSelectedContacts(nextSelectedContacts);
|
||||
return;
|
||||
}
|
||||
const selectedContact = contactLookup.get(conversationId);
|
||||
if (selectedContact) {
|
||||
setSelectedContacts([...nextSelectedContacts, selectedContact]);
|
||||
}
|
||||
},
|
||||
[contactLookup, selectedContacts, setSelectedContacts]
|
||||
);
|
||||
|
||||
const normalizedSearchTerm = searchTerm.trim();
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setFilteredConversations(
|
||||
filterAndSortConversationsByRecent(
|
||||
candidateConversations,
|
||||
normalizedSearchTerm,
|
||||
undefined
|
||||
)
|
||||
);
|
||||
}, 200);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [candidateConversations, normalizedSearchTerm, setFilteredConversations]);
|
||||
|
||||
const resetChooseViewersScreen = useCallback(() => {
|
||||
setSelectedContacts([]);
|
||||
setSearchTerm('');
|
||||
setPage(Page.DistributionLists);
|
||||
}, []);
|
||||
|
||||
const selectedConversationUuids: Set<UUIDStringType> = useMemo(
|
||||
() =>
|
||||
new Set(selectedContacts.map(contact => contact.uuid).filter(isNotNil)),
|
||||
[selectedContacts]
|
||||
);
|
||||
|
||||
const [confirmDeleteListId, setConfirmDeleteListId] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const [confirmRemoveMember, setConfirmRemoveMember] = useState<
|
||||
| undefined
|
||||
| {
|
||||
listId: string;
|
||||
title: string;
|
||||
uuid: UUIDStringType | undefined;
|
||||
}
|
||||
>();
|
||||
|
||||
let content: JSX.Element;
|
||||
if (page === Page.NameStory) {
|
||||
content = (
|
||||
<>
|
||||
<div className="StoriesSettingsModal__name-story-avatar-container">
|
||||
<div className="StoriesSettingsModal__list__avatar--private StoriesSettingsModal__list__avatar--private--large" />
|
||||
</div>
|
||||
|
||||
<Input
|
||||
i18n={i18n}
|
||||
onChange={setStoryName}
|
||||
placeholder={i18n('StoriesSettings__name-placeholder')}
|
||||
value={storyName}
|
||||
/>
|
||||
|
||||
<div className="StoriesSettingsModal__title">
|
||||
{i18n('StoriesSettings__who-can-see')}
|
||||
</div>
|
||||
|
||||
{selectedContacts.map(contact => (
|
||||
<div
|
||||
className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer"
|
||||
key={contact.id}
|
||||
>
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPath={contact.avatarPath}
|
||||
badge={getPreferredBadge(contact.badges)}
|
||||
color={contact.color}
|
||||
conversationType={contact.type}
|
||||
i18n={i18n}
|
||||
isMe
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
theme={ThemeType.dark}
|
||||
title={contact.title}
|
||||
/>
|
||||
<span className="StoriesSettingsModal__list__title">
|
||||
{contact.title}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
} else if (
|
||||
page === Page.AddViewer ||
|
||||
page === Page.ChooseViewers ||
|
||||
page === Page.HideStoryFrom
|
||||
) {
|
||||
const rowCount = filteredConversations.length;
|
||||
const getRow = (index: number): undefined | Row => {
|
||||
const contact = filteredConversations[index];
|
||||
if (!contact || !contact.uuid) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isSelected = selectedConversationUuids.has(
|
||||
UUID.fromString(contact.uuid)
|
||||
);
|
||||
|
||||
return {
|
||||
type: RowType.ContactCheckbox,
|
||||
contact,
|
||||
isChecked: isSelected,
|
||||
};
|
||||
};
|
||||
|
||||
content = (
|
||||
<>
|
||||
<SearchInput
|
||||
disabled={candidateConversations.length === 0}
|
||||
i18n={i18n}
|
||||
placeholder={i18n('contactSearchPlaceholder')}
|
||||
moduleClassName="StoriesSettingsModal__search"
|
||||
onChange={event => {
|
||||
setSearchTerm(event.target.value);
|
||||
}}
|
||||
value={searchTerm}
|
||||
/>
|
||||
{selectedContacts.length ? (
|
||||
<div className="StoriesSettingsModal__tags">
|
||||
{selectedContacts.map(contact => (
|
||||
<div className="StoriesSettingsModal__tag" key={contact.id}>
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPath={contact.avatarPath}
|
||||
badge={getPreferredBadge(contact.badges)}
|
||||
color={contact.color}
|
||||
conversationType={contact.type}
|
||||
i18n={i18n}
|
||||
isMe={contact.isMe}
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={AvatarSize.TWENTY_EIGHT}
|
||||
theme={ThemeType.dark}
|
||||
title={contact.title}
|
||||
/>
|
||||
<span className="StoriesSettingsModal__tag__name">
|
||||
{contact.firstName ||
|
||||
contact.profileName ||
|
||||
contact.phoneNumber}
|
||||
</span>
|
||||
<button
|
||||
aria-label={i18n('StoriesSettings__remove--title', [
|
||||
contact.title,
|
||||
])}
|
||||
className="StoriesSettingsModal__tag__remove"
|
||||
onClick={() => toggleSelectedConversation(contact.id)}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : undefined}
|
||||
{candidateConversations.length ? (
|
||||
<Measure bounds>
|
||||
{({ contentRect, measureRef }: MeasuredComponentProps) => (
|
||||
<div
|
||||
className="StoriesSettingsModal__conversation-list"
|
||||
ref={measureRef}
|
||||
>
|
||||
<ConversationList
|
||||
dimensions={contentRect.bounds}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
getRow={getRow}
|
||||
i18n={i18n}
|
||||
onClickArchiveButton={shouldNeverBeCalled}
|
||||
onClickContactCheckbox={(conversationId: string) => {
|
||||
toggleSelectedConversation(conversationId);
|
||||
}}
|
||||
lookupConversationWithoutUuid={asyncShouldNeverBeCalled}
|
||||
showConversation={shouldNeverBeCalled}
|
||||
showUserNotFoundModal={shouldNeverBeCalled}
|
||||
setIsFetchingUUID={shouldNeverBeCalled}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
renderMessageSearchResult={() => {
|
||||
shouldNeverBeCalled();
|
||||
return <div />;
|
||||
}}
|
||||
rowCount={rowCount}
|
||||
shouldRecomputeRowHeights={false}
|
||||
showChooseGroupMembers={shouldNeverBeCalled}
|
||||
theme={ThemeType.dark}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Measure>
|
||||
) : (
|
||||
<div className="module-ForwardMessageModal__no-candidate-contacts">
|
||||
{i18n('noContactsFound')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else if (listToEdit) {
|
||||
const isMyStories = listToEdit.id === MY_STORIES_ID;
|
||||
|
||||
content = (
|
||||
<>
|
||||
{!isMyStories && (
|
||||
<>
|
||||
<div className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer">
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
<span className="StoriesSettingsModal__list__avatar--private" />
|
||||
<span className="StoriesSettingsModal__list__title">
|
||||
<StoryDistributionListName
|
||||
i18n={i18n}
|
||||
id={listToEdit.id}
|
||||
name={listToEdit.name}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<hr className="StoriesSettingsModal__divider" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="StoriesSettingsModal__title">
|
||||
{i18n('StoriesSettings__who-can-see')}
|
||||
</div>
|
||||
|
||||
{isMyStories && (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={!listToEdit.members.length}
|
||||
description={i18n('StoriesSettings__mine__all--description')}
|
||||
isRadio
|
||||
label={i18n('StoriesSettings__mine__all--label')}
|
||||
moduleClassName="StoriesSettingsModal__checkbox"
|
||||
name="share"
|
||||
onChange={() => {
|
||||
setMyStoriesToAllSignalConnections();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={listToEdit.isBlockList && listToEdit.members.length > 0}
|
||||
description={i18n('StoriesSettings__mine__exclude--description', [
|
||||
listToEdit.isBlockList
|
||||
? String(listToEdit.members.length)
|
||||
: '0',
|
||||
])}
|
||||
isRadio
|
||||
label={i18n('StoriesSettings__mine__exclude--label')}
|
||||
moduleClassName="StoriesSettingsModal__checkbox"
|
||||
name="share"
|
||||
onChange={noop}
|
||||
onClick={() => {
|
||||
if (listToEdit.isBlockList) {
|
||||
setSelectedContacts(listToEdit.members);
|
||||
}
|
||||
setPage(Page.HideStoryFrom);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={!listToEdit.isBlockList && listToEdit.members.length > 0}
|
||||
description={
|
||||
!listToEdit.isBlockList && listToEdit.members.length
|
||||
? i18n('StoriesSettings__mine__only--description--people', [
|
||||
String(listToEdit.members.length),
|
||||
])
|
||||
: i18n('StoriesSettings__mine__only--description')
|
||||
}
|
||||
isRadio
|
||||
label={i18n('StoriesSettings__mine__only--label')}
|
||||
moduleClassName="StoriesSettingsModal__checkbox"
|
||||
name="share"
|
||||
onChange={noop}
|
||||
onClick={() => {
|
||||
if (!listToEdit.isBlockList) {
|
||||
setSelectedContacts(listToEdit.members);
|
||||
}
|
||||
setPage(Page.AddViewer);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="StoriesSettingsModal__disclaimer">
|
||||
<Intl
|
||||
components={{
|
||||
learnMore: (
|
||||
<button
|
||||
className="StoriesSettingsModal__disclaimer__learn-more"
|
||||
onClick={toggleSignalConnectionsModal}
|
||||
type="button"
|
||||
>
|
||||
{i18n('StoriesSettings__mine__disclaimer--learn-more')}
|
||||
</button>
|
||||
),
|
||||
}}
|
||||
i18n={i18n}
|
||||
id="StoriesSettings__mine__disclaimer"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isMyStories && (
|
||||
<>
|
||||
<button
|
||||
className="StoriesSettingsModal__list"
|
||||
onClick={() => {
|
||||
setSelectedContacts(listToEdit.members);
|
||||
setPage(Page.AddViewer);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
<span className="StoriesSettingsModal__list__avatar--new" />
|
||||
<span className="StoriesSettingsModal__list__title">
|
||||
{i18n('StoriesSettings__add-viewer')}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{listToEdit.members.map(member => (
|
||||
<div
|
||||
className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer"
|
||||
key={member.id}
|
||||
>
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
<Avatar
|
||||
acceptedMessageRequest={member.acceptedMessageRequest}
|
||||
avatarPath={member.avatarPath}
|
||||
badge={getPreferredBadge(member.badges)}
|
||||
color={member.color}
|
||||
conversationType={member.type}
|
||||
i18n={i18n}
|
||||
isMe
|
||||
sharedGroupNames={member.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
theme={ThemeType.dark}
|
||||
title={member.title}
|
||||
/>
|
||||
<span className="StoriesSettingsModal__list__title">
|
||||
{member.title}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<button
|
||||
aria-label={i18n('StoriesSettings__remove--title', [
|
||||
member.title,
|
||||
])}
|
||||
className="StoriesSettingsModal__list__delete"
|
||||
onClick={() =>
|
||||
setConfirmRemoveMember({
|
||||
listId: listToEdit.id,
|
||||
title: member.title,
|
||||
uuid: member.uuid,
|
||||
})
|
||||
}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<hr className="StoriesSettingsModal__divider" />
|
||||
|
||||
<div className="StoriesSettingsModal__title">
|
||||
{i18n('StoriesSettings__replies-reactions--title')}
|
||||
</div>
|
||||
|
||||
<Checkbox
|
||||
checked={listToEdit.allowsReplies}
|
||||
description={i18n('StoriesSettings__replies-reactions--description')}
|
||||
label={i18n('StoriesSettings__replies-reactions--label')}
|
||||
moduleClassName="StoriesSettingsModal__checkbox"
|
||||
name="replies-reactions"
|
||||
onChange={value => onRepliesNReactionsChanged(listToEdit.id, value)}
|
||||
/>
|
||||
|
||||
{!isMyStories && (
|
||||
<>
|
||||
<hr className="StoriesSettingsModal__divider" />
|
||||
|
||||
<button
|
||||
className="StoriesSettingsModal__delete-list"
|
||||
onClick={() => setConfirmDeleteListId(listToEdit.id)}
|
||||
type="button"
|
||||
>
|
||||
{i18n('StoriesSettings__delete-list')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const privateStories = distributionLists.filter(
|
||||
list => list.id !== MY_STORIES_ID
|
||||
);
|
||||
|
||||
content = (
|
||||
<>
|
||||
<button
|
||||
className="StoriesSettingsModal__list"
|
||||
onClick={() => {
|
||||
setListToEditId(MY_STORIES_ID);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
<Avatar
|
||||
acceptedMessageRequest={me.acceptedMessageRequest}
|
||||
avatarPath={me.avatarPath}
|
||||
badge={getPreferredBadge(me.badges)}
|
||||
color={me.color}
|
||||
conversationType={me.type}
|
||||
i18n={i18n}
|
||||
isMe
|
||||
sharedGroupNames={me.sharedGroupNames}
|
||||
size={AvatarSize.THIRTY_SIX}
|
||||
theme={ThemeType.dark}
|
||||
title={me.title}
|
||||
/>
|
||||
<span className="StoriesSettingsModal__list__title">
|
||||
{i18n('Stories__mine')}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="StoriesSettingsModal__list__viewers" />
|
||||
</button>
|
||||
|
||||
<hr className="StoriesSettingsModal__divider" />
|
||||
|
||||
<button
|
||||
className="StoriesSettingsModal__list"
|
||||
onClick={() => {
|
||||
setPage(Page.ChooseViewers);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
<span className="StoriesSettingsModal__list__avatar--new" />
|
||||
<span className="StoriesSettingsModal__list__title">
|
||||
{i18n('StoriesSettings__new-list')}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{privateStories.map(list => (
|
||||
<button
|
||||
className="StoriesSettingsModal__list"
|
||||
key={list.id}
|
||||
onClick={() => {
|
||||
setListToEditId(list.id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span className="StoriesSettingsModal__list__left">
|
||||
<span className="StoriesSettingsModal__list__avatar--private" />
|
||||
<span className="StoriesSettingsModal__list__title">
|
||||
{list.name}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="StoriesSettingsModal__list__viewers">
|
||||
{list.members.length === 1
|
||||
? i18n('StoriesSettingsModal__list__viewers--singular', ['1'])
|
||||
: i18n('StoriesSettings__viewers--plural', [
|
||||
String(list.members.length),
|
||||
])}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const isChoosingViewers =
|
||||
page === Page.ChooseViewers || page === Page.AddViewer;
|
||||
|
||||
let modalTitle: string = i18n('StoriesSettings__title');
|
||||
if (page === Page.HideStoryFrom) {
|
||||
modalTitle = i18n('StoriesSettings__hide-story');
|
||||
} else if (page === Page.NameStory) {
|
||||
modalTitle = i18n('StoriesSettings__name-story');
|
||||
} else if (isChoosingViewers) {
|
||||
modalTitle = i18n('StoriesSettings__choose-viewers');
|
||||
} else if (listToEdit) {
|
||||
modalTitle = getStoryDistributionListName(
|
||||
i18n,
|
||||
listToEdit.id,
|
||||
listToEdit.name
|
||||
);
|
||||
}
|
||||
|
||||
const hasBackButton = page !== Page.DistributionLists || listToEdit;
|
||||
const hasStickyButtons =
|
||||
isChoosingViewers || page === Page.NameStory || page === Page.HideStoryFrom;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
hasStickyButtons={hasStickyButtons}
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
moduleClassName="StoriesSettingsModal__modal"
|
||||
onBackButtonClick={
|
||||
hasBackButton
|
||||
? () => {
|
||||
if (page === Page.HideStoryFrom) {
|
||||
resetChooseViewersScreen();
|
||||
} else if (page === Page.NameStory) {
|
||||
setPage(Page.ChooseViewers);
|
||||
} else if (isChoosingViewers) {
|
||||
resetChooseViewersScreen();
|
||||
} else if (listToEdit) {
|
||||
setListToEditId(undefined);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onClose={hideStoriesSettings}
|
||||
theme={Theme.Dark}
|
||||
title={modalTitle}
|
||||
>
|
||||
{content}
|
||||
{isChoosingViewers && (
|
||||
<Modal.ButtonFooter>
|
||||
<Button
|
||||
disabled={selectedContacts.length === 0}
|
||||
onClick={() => {
|
||||
if (listToEdit && page === Page.AddViewer) {
|
||||
onViewersUpdated(
|
||||
listToEdit.id,
|
||||
Array.from(selectedConversationUuids)
|
||||
);
|
||||
resetChooseViewersScreen();
|
||||
}
|
||||
|
||||
if (page === Page.ChooseViewers) {
|
||||
setPage(Page.NameStory);
|
||||
}
|
||||
}}
|
||||
variant={ButtonVariant.Primary}
|
||||
>
|
||||
{page === Page.AddViewer ? i18n('done') : i18n('next2')}
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
)}
|
||||
{page === Page.NameStory && (
|
||||
<Modal.ButtonFooter>
|
||||
<Button
|
||||
disabled={!storyName}
|
||||
onClick={() => {
|
||||
onDistributionListCreated(
|
||||
storyName,
|
||||
Array.from(selectedConversationUuids)
|
||||
);
|
||||
setStoryName('');
|
||||
resetChooseViewersScreen();
|
||||
}}
|
||||
variant={ButtonVariant.Primary}
|
||||
>
|
||||
{i18n('done')}
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
)}
|
||||
{page === Page.HideStoryFrom && (
|
||||
<Modal.ButtonFooter>
|
||||
<Button
|
||||
disabled={selectedContacts.length === 0}
|
||||
onClick={() => {
|
||||
onHideMyStoriesFrom(Array.from(selectedConversationUuids));
|
||||
resetChooseViewersScreen();
|
||||
}}
|
||||
variant={ButtonVariant.Primary}
|
||||
>
|
||||
{i18n('update')}
|
||||
</Button>
|
||||
</Modal.ButtonFooter>
|
||||
)}
|
||||
</Modal>
|
||||
{confirmDeleteListId && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
action: () => {
|
||||
onDeleteList(confirmDeleteListId);
|
||||
setListToEditId(undefined);
|
||||
},
|
||||
style: 'negative',
|
||||
text: i18n('delete'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setConfirmDeleteListId(undefined);
|
||||
}}
|
||||
>
|
||||
{i18n('StoriesSettings__delete-list--confirm')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
{confirmRemoveMember && (
|
||||
<ConfirmationDialog
|
||||
actions={[
|
||||
{
|
||||
action: () =>
|
||||
onRemoveMember(
|
||||
confirmRemoveMember.listId,
|
||||
confirmRemoveMember.uuid
|
||||
),
|
||||
style: 'negative',
|
||||
text: i18n('StoriesSettings__remove--action'),
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
setConfirmRemoveMember(undefined);
|
||||
}}
|
||||
title={i18n('StoriesSettings__remove--title', [
|
||||
confirmRemoveMember.title,
|
||||
])}
|
||||
>
|
||||
{i18n('StoriesSettings__remove--body')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
21
ts/components/StoryDistributionListName.tsx
Normal file
21
ts/components/StoryDistributionListName.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { getStoryDistributionListName } from '../types/Stories';
|
||||
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const StoryDistributionListName = ({
|
||||
i18n,
|
||||
id,
|
||||
name,
|
||||
}: PropsType): JSX.Element => {
|
||||
return <>{getStoryDistributionListName(i18n, id, name)}</>;
|
||||
};
|
|
@ -15,7 +15,6 @@ import Measure from 'react-measure';
|
|||
|
||||
import type { LocalizerType, ThemeType } from '../../../../types/Util';
|
||||
import { getUsernameFromSearch } from '../../../../types/Username';
|
||||
import { assert } from '../../../../util/assert';
|
||||
import { refMerger } from '../../../../util/refMerger';
|
||||
import { useRestoreFocus } from '../../../../hooks/useRestoreFocus';
|
||||
import { missingCaseError } from '../../../../util/missingCaseError';
|
||||
|
@ -40,6 +39,7 @@ import { ConversationList, RowType } from '../../../ConversationList';
|
|||
import { ContactCheckboxDisabledReason } from '../../../conversationList/ContactCheckbox';
|
||||
import { Button, ButtonVariant } from '../../../Button';
|
||||
import { SearchInput } from '../../../SearchInput';
|
||||
import { shouldNeverBeCalled } from '../../../../util/shouldNeverBeCalled';
|
||||
|
||||
export type StatePropsType = {
|
||||
regionCode: string | undefined;
|
||||
|
@ -399,7 +399,3 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
</ModalHost>
|
||||
);
|
||||
};
|
||||
|
||||
function shouldNeverBeCalled(..._args: ReadonlyArray<unknown>): unknown {
|
||||
assert(false, 'This should never be called. Doing nothing');
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue