diff --git a/web/components/stepper.react.js b/web/components/stepper.react.js
index b824e44dd..7b52a21b4 100644
--- a/web/components/stepper.react.js
+++ b/web/components/stepper.react.js
@@ -1,108 +1,108 @@
// @flow
import classnames from 'classnames';
import * as React from 'react';
import Button from './button.react.js';
import css from './stepper.css';
import LoadingIndicator from '../loading-indicator.react.js';
export type ButtonProps = {
+content: React.Node,
+disabled?: boolean,
+loading?: boolean,
- +onClick: () => void,
+ +onClick: () => mixed,
};
type ButtonType = 'prev' | 'next';
type ActionButtonProps = {
+buttonProps: ButtonProps,
+type: ButtonType,
};
function ActionButton(props: ActionButtonProps) {
const { buttonProps, type } = props;
const { content, loading, disabled, onClick } = buttonProps;
const buttonContent = loading ? (
<>
{content}
>
) : (
content
);
return (
);
}
type ItemProps = {
+content: React.Node,
+name: string,
+errorMessage?: string,
+prevProps?: ButtonProps,
+nextProps?: ButtonProps,
};
function StepperItem(props: ItemProps): React.Node {
const { content, errorMessage, prevProps, nextProps } = props;
const prevButton = React.useMemo(
() =>
prevProps ? : null,
[prevProps],
);
const nextButton = React.useMemo(
() =>
nextProps ? : null,
[nextProps],
);
return (
<>
{content}
{errorMessage}
{prevButton}
{nextButton}
>
);
}
type ContainerProps = {
+activeStep: string,
+className?: string,
+children: React.ChildrenArray>,
};
function StepperContainer(props: ContainerProps): React.Node {
const { children, activeStep, className = '' } = props;
const index = new Map(
React.Children.toArray(children).map(child => [child.props.name, child]),
);
const activeComponent = index.get(activeStep);
const styles = classnames(css.stepperContainer, className);
return {activeComponent}
;
}
const Stepper = {
Container: StepperContainer,
Item: StepperItem,
};
export default Stepper;
diff --git a/web/modals/threads/create/compose-subchannel-modal.react.js b/web/modals/threads/create/compose-subchannel-modal.react.js
index 635024369..67156ac35 100644
--- a/web/modals/threads/create/compose-subchannel-modal.react.js
+++ b/web/modals/threads/create/compose-subchannel-modal.react.js
@@ -1,278 +1,276 @@
// @flow
import * as React from 'react';
import {
useNewThread,
newThreadActionTypes,
} from 'lib/actions/thread-actions.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import { threadTypes } from 'lib/types/thread-types-enum.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import { useDispatchActionPromise } from 'lib/utils/action-utils.js';
import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.js';
import { useDispatch } from 'lib/utils/redux-utils.js';
import { trimText } from 'lib/utils/text-utils.js';
import css from './compose-subchannel-modal.css';
import SubchannelMembers from './steps/subchannel-members.react.js';
import SubchannelSettings from './steps/subchannel-settings.react.js';
import type { VisibilityType } from './steps/subchannel-settings.react.js';
import Stepper from '../../../components/stepper.react.js';
import { updateNavInfoActionType } from '../../../redux/action-types.js';
import { useSelector } from '../../../redux/redux-utils.js';
import { nonThreadCalendarQuery } from '../../../selectors/nav-selectors.js';
import Modal from '../../modal.react.js';
type Props = {
+onClose: () => void,
+parentThreadInfo: ThreadInfo,
};
const getThreadType = (visibility: VisibilityType, announcement: boolean) => {
if (visibility === 'open') {
return announcement
? threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD
: threadTypes.COMMUNITY_OPEN_SUBTHREAD;
} else {
return announcement
? threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD
: threadTypes.COMMUNITY_SECRET_SUBTHREAD;
}
};
type Steps = 'settings' | 'members';
type HeaderProps = {
+parentThreadName: string,
};
function ComposeSubchannelHeader(props: HeaderProps): React.Node {
const { parentThreadName } = props;
return (
{'within '}
{parentThreadName}
);
}
const createSubchannelLoadingStatusSelector =
createLoadingStatusSelector(newThreadActionTypes);
function ComposeSubchannelModal(props: Props): React.Node {
const { parentThreadInfo, onClose } = props;
const { uiName: parentThreadName } = useResolvedThreadInfo(parentThreadInfo);
const [activeStep, setActiveStep] = React.useState('settings');
const [channelName, setChannelName] = React.useState('');
const [visibilityType, setVisibilityType] =
React.useState('open');
const [announcement, setAnnouncement] = React.useState(false);
const [selectedUsers, setSelectedUsers] = React.useState<
$ReadOnlySet,
>(new Set());
const [searchUserText, setSearchUserText] = React.useState('');
const loadingState = useSelector(createSubchannelLoadingStatusSelector);
const [errorMessage, setErrorMessage] = React.useState('');
const calendarQuery = useSelector(nonThreadCalendarQuery);
const callNewThread = useNewThread();
const dispatchActionPromise = useDispatchActionPromise();
const dispatch = useDispatch();
const createSubchannel = React.useCallback(async () => {
try {
const threadType = getThreadType(visibilityType, announcement);
const query = calendarQuery();
const result = await callNewThread({
name: channelName,
type: threadType,
parentThreadID: parentThreadInfo.id,
initialMemberIDs: Array.from(selectedUsers),
calendarQuery: query,
color: parentThreadInfo.color,
});
return result;
} catch (e) {
await setErrorMessage('unknown error');
return null;
}
}, [
parentThreadInfo,
selectedUsers,
visibilityType,
announcement,
callNewThread,
calendarQuery,
channelName,
]);
const dispatchCreateSubchannel = React.useCallback(async () => {
await setErrorMessage('');
const response = createSubchannel();
await dispatchActionPromise(newThreadActionTypes, response);
const result = await response;
if (result) {
const { newThreadID } = result;
await dispatch({
type: updateNavInfoActionType,
payload: {
activeChatThreadID: newThreadID,
},
});
props.onClose();
}
}, [dispatchActionPromise, createSubchannel, props, dispatch]);
const onChangeChannelName = React.useCallback(
(event: SyntheticEvent) => {
const target = event.currentTarget;
setChannelName(target.value);
},
[],
);
const onOpenVisibilityTypeSelected = React.useCallback(
() => setVisibilityType('open'),
[],
);
const onSecretVisibilityTypeSelected = React.useCallback(
() => setVisibilityType('secret'),
[],
);
const onAnnouncementSelected = React.useCallback(
() => setAnnouncement(!announcement),
[announcement],
);
const toggleUserSelection = React.useCallback((userID: string) => {
setSelectedUsers((users: $ReadOnlySet) => {
const newUsers = new Set(users);
if (newUsers.has(userID)) {
newUsers.delete(userID);
} else {
newUsers.add(userID);
}
return newUsers;
});
}, []);
const subchannelSettings = React.useMemo(
() => (
),
[
channelName,
visibilityType,
announcement,
onChangeChannelName,
onOpenVisibilityTypeSelected,
onSecretVisibilityTypeSelected,
onAnnouncementSelected,
],
);
const stepperButtons = React.useMemo(
() => ({
settings: {
nextProps: {
content: 'Next',
disabled: !channelName.trim(),
onClick: () => {
setErrorMessage('');
setChannelName(channelName.trim());
setActiveStep('members');
},
},
},
members: {
prevProps: {
content: 'Back',
onClick: () => setActiveStep('settings'),
},
nextProps: {
content: 'Create',
loading: loadingState === 'loading',
disabled: selectedUsers.size === 0,
- onClick: () => {
- dispatchCreateSubchannel();
- },
+ onClick: dispatchCreateSubchannel,
},
},
}),
[channelName, dispatchCreateSubchannel, loadingState, selectedUsers],
);
const subchannelMembers = React.useMemo(
() => (
),
[
selectedUsers,
toggleUserSelection,
parentThreadInfo,
searchUserText,
setSearchUserText,
],
);
const modalName =
activeStep === 'members'
? `Create channel - ${trimText(channelName, 11)}`
: 'Create channel';
return (
);
}
export default ComposeSubchannelModal;