diff --git a/keyserver/src/responders/thread-responders.js b/keyserver/src/responders/thread-responders.js index 09b02798a..8986d2107 100644 --- a/keyserver/src/responders/thread-responders.js +++ b/keyserver/src/responders/thread-responders.js @@ -1,337 +1,339 @@ // @flow import t from 'tcomb'; import type { TInterface, TUnion } from 'tcomb'; import { mediaValidator } from 'lib/types/media-types.js'; import { rawMessageInfoValidator, messageTruncationStatusesValidator, } from 'lib/types/message-types.js'; import { userSurfacedPermissionValidator } from 'lib/types/thread-permission-types.js'; import { threadTypes } from 'lib/types/thread-types-enum.js'; import { type ThreadDeletionRequest, type RoleChangeRequest, type ChangeThreadSettingsResult, type RemoveMembersRequest, type LeaveThreadRequest, type LeaveThreadResult, type UpdateThreadRequest, type ServerNewThreadRequest, type NewThreadResponse, type ServerThreadJoinRequest, type ThreadJoinResult, type ThreadFetchMediaResult, type ThreadFetchMediaRequest, type ToggleMessagePinRequest, type ToggleMessagePinResult, type RoleModificationRequest, type RoleModificationResult, type RoleDeletionRequest, type RoleDeletionResult, rawThreadInfoValidator, } from 'lib/types/thread-types.js'; import { serverUpdateInfoValidator } from 'lib/types/update-types.js'; import { userInfosValidator } from 'lib/types/user-types.js'; import { updateUserAvatarRequestValidator } from 'lib/utils/avatar-utils.js'; import { values } from 'lib/utils/objects.js'; import { tShape, tNumEnum, tColor, tPassword, tID, } from 'lib/utils/validation-utils.js'; import { entryQueryInputValidator, verifyCalendarQueryThreadIDs, } from './entry-responders.js'; import { modifyRole } from '../creators/role-creator.js'; import { createThread } from '../creators/thread-creator.js'; import { deleteRole } from '../deleters/role-deleters.js'; import { deleteThread } from '../deleters/thread-deleters.js'; import { fetchMediaForThread } from '../fetchers/upload-fetchers.js'; import type { Viewer } from '../session/viewer.js'; import { updateRole, removeMembers, leaveThread, updateThread, joinThread, toggleMessagePinForThread, } from '../updaters/thread-updaters.js'; export const threadDeletionRequestInputValidator: TInterface = tShape({ threadID: tID, accountPassword: t.maybe(tPassword), }); export const leaveThreadResultValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function threadDeletionResponder( viewer: Viewer, request: ThreadDeletionRequest, ): Promise { return await deleteThread(viewer, request); } export const roleChangeRequestInputValidator: TInterface = tShape({ threadID: tID, memberIDs: t.list(t.String), role: t.refinement(tID, str => { if (str.indexOf('|') !== -1) { str = str.split('|')[1]; } const int = parseInt(str, 10); return String(int) === str && int > 0; }), }); export const changeThreadSettingsResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function roleUpdateResponder( viewer: Viewer, request: RoleChangeRequest, ): Promise { return await updateRole(viewer, request); } export const removeMembersRequestInputValidator: TInterface = tShape({ threadID: tID, memberIDs: t.list(t.String), }); async function memberRemovalResponder( viewer: Viewer, request: RemoveMembersRequest, ): Promise { return await removeMembers(viewer, request); } export const leaveThreadRequestInputValidator: TInterface = tShape({ threadID: tID, }); async function threadLeaveResponder( viewer: Viewer, request: LeaveThreadRequest, ): Promise { return await leaveThread(viewer, request); } export const updateThreadRequestInputValidator: TInterface = tShape({ threadID: tID, changes: tShape({ type: t.maybe(tNumEnum(values(threadTypes))), name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), parentThreadID: t.maybe(tID), newMemberIDs: t.maybe(t.list(t.String)), avatar: t.maybe(updateUserAvatarRequestValidator), }), accountPassword: t.maybe(tPassword), }); async function threadUpdateResponder( viewer: Viewer, request: UpdateThreadRequest, ): Promise { return await updateThread(viewer, request); } const threadRequestValidationShape = { name: t.maybe(t.String), description: t.maybe(t.String), color: t.maybe(tColor), parentThreadID: t.maybe(tID), initialMemberIDs: t.maybe(t.list(t.String)), calendarQuery: t.maybe(entryQueryInputValidator), }; const newThreadRequestInputValidator: TUnion = t.union([ tShape({ type: tNumEnum([threadTypes.SIDEBAR]), sourceMessageID: tID, ...threadRequestValidationShape, }), tShape({ type: tNumEnum([ threadTypes.COMMUNITY_OPEN_SUBTHREAD, threadTypes.COMMUNITY_SECRET_SUBTHREAD, threadTypes.PERSONAL, threadTypes.LOCAL, threadTypes.COMMUNITY_ROOT, threadTypes.COMMUNITY_ANNOUNCEMENT_ROOT, + threadTypes.COMMUNITY_OPEN_ANNOUNCEMENT_SUBTHREAD, + threadTypes.COMMUNITY_SECRET_ANNOUNCEMENT_SUBTHREAD, ]), ...threadRequestValidationShape, }), ]); export const newThreadResponseValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), newMessageInfos: t.list(rawMessageInfoValidator), userInfos: userInfosValidator, newThreadID: tID, }); async function threadCreationResponder( viewer: Viewer, request: ServerNewThreadRequest, ): Promise { return await createThread(viewer, request, { silentlyFailMembers: request.type === threadTypes.SIDEBAR, }); } export const joinThreadRequestInputValidator: TInterface = tShape({ threadID: tID, calendarQuery: t.maybe(entryQueryInputValidator), inviteLinkSecret: t.maybe(t.String), }); export const threadJoinResultValidator: TInterface = tShape({ updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), rawMessageInfos: t.list(rawMessageInfoValidator), truncationStatuses: messageTruncationStatusesValidator, userInfos: userInfosValidator, }); async function threadJoinResponder( viewer: Viewer, request: ServerThreadJoinRequest, ): Promise { if (request.calendarQuery) { await verifyCalendarQueryThreadIDs(request.calendarQuery); } return await joinThread(viewer, request); } export const threadFetchMediaRequestInputValidator: TInterface = tShape({ threadID: tID, limit: t.Number, offset: t.Number, }); export const threadFetchMediaResultValidator: TInterface = tShape({ media: t.list(mediaValidator) }); async function threadFetchMediaResponder( viewer: Viewer, request: ThreadFetchMediaRequest, ): Promise { return await fetchMediaForThread(viewer, request); } export const toggleMessagePinRequestInputValidator: TInterface = tShape({ messageID: tID, action: t.enums.of(['pin', 'unpin']), }); export const toggleMessagePinResultValidator: TInterface = tShape({ newMessageInfos: t.list(rawMessageInfoValidator), threadID: tID, }); async function toggleMessagePinResponder( viewer: Viewer, request: ToggleMessagePinRequest, ): Promise { return await toggleMessagePinForThread(viewer, request); } export const roleModificationRequestInputValidator: TUnion = t.union([ tShape({ community: tID, name: t.String, permissions: t.list(userSurfacedPermissionValidator), action: t.enums.of(['create_role']), }), tShape({ community: tID, existingRoleID: tID, name: t.String, permissions: t.list(userSurfacedPermissionValidator), action: t.enums.of(['edit_role']), }), ]); export const roleModificationResultValidator: TInterface = tShape({ threadInfo: t.maybe(rawThreadInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function roleModificationResponder( viewer: Viewer, request: RoleModificationRequest, ): Promise { return await modifyRole(viewer, request); } export const roleDeletionRequestInputValidator: TInterface = tShape({ community: tID, roleID: tID, }); export const roleDeletionResultValidator: TInterface = tShape({ threadInfo: t.maybe(rawThreadInfoValidator), updatesResult: tShape({ newUpdates: t.list(serverUpdateInfoValidator), }), }); async function roleDeletionResponder( viewer: Viewer, request: RoleDeletionRequest, ): Promise { return await deleteRole(viewer, request); } export { threadDeletionResponder, roleUpdateResponder, memberRemovalResponder, threadLeaveResponder, threadUpdateResponder, threadCreationResponder, threadJoinResponder, threadFetchMediaResponder, newThreadRequestInputValidator, toggleMessagePinResponder, roleModificationResponder, roleDeletionResponder, }; diff --git a/web/modals/threads/create/compose-subchannel-modal.react.js b/web/modals/threads/create/compose-subchannel-modal.react.js index 6471d5a96..ce95ee9c6 100644 --- a/web/modals/threads/create/compose-subchannel-modal.react.js +++ b/web/modals/threads/create/compose-subchannel-modal.react.js @@ -1,281 +1,276 @@ // @flow import * as React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { newThread, 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, useServerCall, } from 'lib/utils/action-utils.js'; import { useResolvedThreadInfo } from 'lib/utils/entity-helpers.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 { 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 = useServerCall(newThread); 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( - e.message === 'invalid_parameters' && announcement - ? 'announcement channels currently not available' - : 'unknown error', - ); - + 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(); }, }, }, }), [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;