diff --git a/native/roles/change-roles-screen.react.js b/native/roles/change-roles-screen.react.js index 44295ab38..b709db070 100644 --- a/native/roles/change-roles-screen.react.js +++ b/native/roles/change-roles-screen.react.js @@ -1,304 +1,303 @@ // @flow import { useActionSheet } from '@expo/react-native-action-sheet'; import invariant from 'invariant'; import * as React from 'react'; import { View, Text, Platform, ActivityIndicator } from 'react-native'; import { TouchableOpacity } from 'react-native-gesture-handler'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { changeThreadMemberRolesActionTypes } from 'lib/actions/thread-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { otherUsersButNoOtherAdmins } from 'lib/selectors/thread-selectors.js'; import { roleIsAdminRole } from 'lib/shared/thread-utils.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { RelativeMemberInfo, ThreadInfo } from 'lib/types/thread-types.js'; import { values } from 'lib/utils/objects.js'; import ChangeRolesHeaderRightButton from './change-roles-header-right-button.react.js'; import UserAvatar from '../avatars/user-avatar.react.js'; import type { ChatNavigationProp } from '../chat/chat.react'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; export type ChangeRolesScreenParams = { +threadInfo: ThreadInfo, +memberInfo: RelativeMemberInfo, +role: ?string, }; type Props = { +navigation: ChatNavigationProp<'ChangeRolesScreen'>, +route: NavigationRoute<'ChangeRolesScreen'>, }; const changeRolesLoadingStatusSelector = createLoadingStatusSelector( changeThreadMemberRolesActionTypes, ); function ChangeRolesScreen(props: Props): React.Node { const { navigation, route } = props; const { threadInfo, memberInfo, role } = props.route.params; invariant(role, 'Role must be defined'); const changeRolesLoadingStatus: LoadingStatus = useSelector( changeRolesLoadingStatusSelector, ); - const activityIndicatorStyle = React.useMemo( - () => ({ paddingRight: 15 }), - [], - ); const styles = useStyles(unboundStyles); const [selectedRole, setSelectedRole] = React.useState(role); const roleOptions = React.useMemo( () => values(threadInfo.roles).map(threadRole => ({ id: threadRole.id, name: threadRole.name, })), [threadInfo.roles], ); const selectedRoleName = React.useMemo( () => roleOptions.find(roleOption => roleOption.id === selectedRole)?.name, [roleOptions, selectedRole], ); const onRoleChange = React.useCallback( (selectedIndex: ?number) => { if ( selectedIndex === undefined || selectedIndex === null || selectedIndex === roleOptions.length ) { return; } const newRole = roleOptions[selectedIndex].id; setSelectedRole(newRole); navigation.setParams({ threadInfo, memberInfo, role: newRole, }); }, [navigation, setSelectedRole, roleOptions, memberInfo, threadInfo], ); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const { showActionSheetWithOptions } = useActionSheet(); const insets = useSafeAreaInsets(); const showActionSheet = React.useCallback(() => { const options = Platform.OS === 'ios' ? [...roleOptions.map(roleOption => roleOption.name), 'Cancel'] : [...roleOptions.map(roleOption => roleOption.name)]; const cancelButtonIndex = Platform.OS === 'ios' ? options.length - 1 : -1; const containerStyle = { paddingBottom: insets.bottom, }; showActionSheetWithOptions( { options, cancelButtonIndex, containerStyle, userInterfaceStyle: activeTheme ?? 'dark', }, onRoleChange, ); }, [ roleOptions, onRoleChange, insets.bottom, activeTheme, showActionSheetWithOptions, ]); const otherUsersButNoOtherAdminsValue = useSelector( otherUsersButNoOtherAdmins(threadInfo.id), ); const memberIsAdmin = React.useMemo(() => { invariant(memberInfo.role, 'Expected member role to be defined'); return roleIsAdminRole(threadInfo.roles[memberInfo.role]); }, [threadInfo.roles, memberInfo.role]); const shouldRoleChangeBeDisabled = React.useMemo( () => otherUsersButNoOtherAdminsValue && memberIsAdmin, [otherUsersButNoOtherAdminsValue, memberIsAdmin], ); const roleSelector = React.useMemo(() => { if (shouldRoleChangeBeDisabled) { return ( {selectedRoleName} ); } return ( {selectedRoleName} ); }, [showActionSheet, styles, selectedRoleName, shouldRoleChangeBeDisabled]); const disabledRoleChangeMessage = React.useMemo(() => { if (!shouldRoleChangeBeDisabled) { return null; } return ( There must be at least one admin at any given time in a community. ); }, [ shouldRoleChangeBeDisabled, styles.disabledWarningBackground, styles.infoIcon, styles.disabledWarningText, ]); React.useEffect(() => { navigation.setOptions({ // eslint-disable-next-line react/display-name headerRight: () => { if (changeRolesLoadingStatus === 'loading') { return ( ); } return ( ); }, }); }, [ changeRolesLoadingStatus, navigation, - activityIndicatorStyle, + styles.activityIndicator, route, shouldRoleChangeBeDisabled, ]); return ( Members can only be assigned one role at a time. Changing a member’s role will replace their previously assigned role. {memberInfo.username} {roleSelector} {disabledRoleChangeMessage} ); } const unboundStyles = { descriptionBackground: { backgroundColor: 'panelForeground', marginBottom: 20, }, descriptionText: { color: 'panelBackgroundLabel', padding: 16, fontSize: 14, }, memberInfo: { backgroundColor: 'panelForeground', padding: 16, marginBottom: 30, display: 'flex', flexDirection: 'column', alignItems: 'center', }, memberInfoUsername: { color: 'panelForegroundLabel', marginTop: 8, fontSize: 18, fontWeight: '500', }, roleSelectorLabel: { color: 'panelForegroundSecondaryLabel', marginLeft: 8, fontSize: 12, }, roleSelector: { backgroundColor: 'panelForeground', marginTop: 8, padding: 16, display: 'flex', alignItems: 'center', flexDirection: 'row', justifyContent: 'space-between', }, currentRole: { color: 'panelForegroundSecondaryLabel', fontSize: 16, }, disabledCurrentRole: { color: 'disabledButton', fontSize: 16, }, pencilIcon: { color: 'panelInputSecondaryForeground', }, disabledPencilIcon: { color: 'disabledButton', }, disabledWarningBackground: { backgroundColor: 'disabledButton', padding: 16, display: 'flex', marginTop: 20, flexDirection: 'row', justifyContent: 'center', width: '75%', alignSelf: 'center', }, disabledWarningText: { color: 'panelForegroundSecondaryLabel', fontSize: 14, marginRight: 8, display: 'flex', }, infoIcon: { color: 'panelForegroundSecondaryLabel', marginRight: 8, marginLeft: 8, marginBottom: 12, }, + activityIndicator: { + paddingRight: 15, + }, }; export default ChangeRolesScreen; diff --git a/web/modals/threads/members/change-member-role-modal.react.js b/web/modals/threads/members/change-member-role-modal.react.js index 1e702a3c8..d4bfb8ca5 100644 --- a/web/modals/threads/members/change-member-role-modal.react.js +++ b/web/modals/threads/members/change-member-role-modal.react.js @@ -1,163 +1,152 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { changeThreadMemberRoles, changeThreadMemberRolesActionTypes, } from 'lib/actions/thread-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { otherUsersButNoOtherAdmins } from 'lib/selectors/thread-selectors.js'; import { roleIsAdminRole } from 'lib/shared/thread-utils.js'; import type { RelativeMemberInfo, ThreadInfo } from 'lib/types/thread-types'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; import { values } from 'lib/utils/objects.js'; import css from './change-member-role-modal.css'; import UserAvatar from '../../../avatars/user-avatar.react.js'; import Button, { buttonThemes } from '../../../components/button.react.js'; import Dropdown from '../../../components/dropdown.react.js'; import { useSelector } from '../../../redux/redux-utils.js'; import Modal from '../../modal.react.js'; import UnsavedChangesModal from '../../unsaved-changes-modal.react.js'; type ChangeMemberRoleModalProps = { +memberInfo: RelativeMemberInfo, +threadInfo: ThreadInfo, }; function ChangeMemberRoleModal(props: ChangeMemberRoleModalProps): React.Node { const { memberInfo, threadInfo } = props; const { pushModal, popModal } = useModalContext(); const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadMemberRoles = useServerCall(changeThreadMemberRoles); const otherUsersButNoOtherAdminsValue = useSelector( otherUsersButNoOtherAdmins(threadInfo.id), ); const roleOptions = React.useMemo( () => values(threadInfo.roles).map(role => ({ id: role.id, name: role.name, })), [threadInfo.roles], ); const initialSelectedRole = memberInfo.role; invariant(initialSelectedRole, "Member's role must be defined"); const [selectedRole, setSelectedRole] = React.useState(initialSelectedRole); const onCloseModal = React.useCallback(() => { if (selectedRole === initialSelectedRole) { popModal(); return; } pushModal(); }, [initialSelectedRole, popModal, pushModal, selectedRole]); const disabledRoleChangeMessage = React.useMemo(() => { const memberIsAdmin = roleIsAdminRole( threadInfo.roles[initialSelectedRole], ); if (!otherUsersButNoOtherAdminsValue || !memberIsAdmin) { return null; } return ( - <> -
- -
- There must be at least one admin at any given time in a community. -
+
+ +
+ There must be at least one admin at any given time in a community.
- +
); }, [initialSelectedRole, otherUsersButNoOtherAdminsValue, threadInfo.roles]); const onSave = React.useCallback(() => { if (selectedRole === initialSelectedRole) { popModal(); return; } - const createChangeThreadMemberRolesPromise = () => { - return callChangeThreadMemberRoles( - threadInfo.id, - [memberInfo.id], - selectedRole, - ); - }; + const createChangeThreadMemberRolesPromise = () => + callChangeThreadMemberRoles(threadInfo.id, [memberInfo.id], selectedRole); dispatchActionPromise( changeThreadMemberRolesActionTypes, createChangeThreadMemberRolesPromise(), ); popModal(); }, [ callChangeThreadMemberRoles, dispatchActionPromise, initialSelectedRole, memberInfo.id, popModal, selectedRole, threadInfo.id, ]); return (
Members can only be assigned to one role at a time. Changing a member’s role will replace their previously assigned role.
{memberInfo.username}
{disabledRoleChangeMessage}
); } export default ChangeMemberRoleModal;