diff --git a/lib/types/thread-permission-types.js b/lib/types/thread-permission-types.js --- a/lib/types/thread-permission-types.js +++ b/lib/types/thread-permission-types.js @@ -6,6 +6,10 @@ import { values } from '../utils/objects.js'; import { tBool, tShape, tID } from '../utils/validation-utils.js'; +// When a new permission is added, if it should be configurable for a role, it +// should be either added to an existing set or a new set alongside a +// new user-facing permission. If it is a permission that should be ensured +// across all roles, it should be added to `universalCommunityPermissions`. export const threadPermissions = Object.freeze({ KNOW_OF: 'know_of', MEMBERSHIP_DEPRECATED: 'membership', @@ -84,6 +88,290 @@ export type ThreadPermissionFilterPrefix = $Values< typeof threadPermissionFilterPrefixes, >; + +// These are the set of user-facing permissions that we display as configurable +// to the user when they are creating a custom role for their given community. +// They are per-community rather than per-thread, so when configured they are +// to be expected to be propagated across the community. Also notably, +// `threadPermissions` is used on the keyserver for permission checks to +// validate actions, but these `userSurfacedPermissions` are only used +// on the client for the UI and propagated to the server. The +// `configurableCommunityPermissions` mapping below is the association between +// each userSurfacedPermission and a set of threadPermissions. +export const userSurfacedPermissions = Object.freeze({ + EDIT_CALENDAR: 'edit_calendar', + KNOW_OF_SECRET_CHANNELS: 'know_of_secret_channels', + VOICED_IN_ANNOUNCEMENT_CHANNELS: 'voiced_in_announcement_channels', + CREATE_AND_EDIT_CHANNELS: 'create_and_edit_channels', + DELETE_CHANNELS: 'delete_channels', + ADD_MEMBERS: 'add_members', + REMOVE_MEMBERS: 'remove_members', + CHANGE_ROLES: 'change_roles', + EDIT_VISIBILITY: 'edit_visibility', + MANAGE_PINS: 'manage_pins', + REACT_TO_MESSAGES: 'react_to_messages', + EDIT_MESSAGES: 'edit_messages', + MANAGE_INVITE_LINKS: 'manage_invite_links', +}); +export type UserSurfacedPermission = $Values; + +const editCalendarPermission = { + title: 'Edit calendar', + description: 'Allows members to edit the community calendar', + userSurfacedPermission: userSurfacedPermissions.EDIT_CALENDAR, +}; +const editEntries = threadPermissions.EDIT_ENTRIES; +const descendantEditEntries = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.EDIT_ENTRIES; +const editCalendarPermissions = new Set([editEntries, descendantEditEntries]); + +const knowOfSecretChannelsPermission = { + title: 'Know of secret channels', + description: 'Allows members to know of all secret channels', + userSurfacedPermission: userSurfacedPermissions.KNOW_OF_SECRET_CHANNELS, +}; +const descendantKnowOf = + threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.KNOW_OF; +const descendantVisible = + threadPermissionPropagationPrefixes.DESCENDANT + threadPermissions.VISIBLE; +const descendantTopLevelJoinThread = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissionFilterPrefixes.TOP_LEVEL + + threadPermissions.JOIN_THREAD; +const descendantJoinThread = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.JOIN_THREAD; +const childJoinThread = + threadPermissionPropagationPrefixes.CHILD + threadPermissions.JOIN_THREAD; +const knowOfSecretChannelsPermissions = new Set([ + descendantKnowOf, + descendantVisible, + descendantTopLevelJoinThread, + descendantJoinThread, + childJoinThread, +]); + +const voicedPermission = { + title: 'Voiced in announcement channels', + description: 'Allows members to send messages in announcement channels', + userSurfacedPermission: + userSurfacedPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS, +}; +const voiced = threadPermissions.VOICED; +const voicedPermissions = new Set([voiced]); + +const createAndEditChannelsPermission = { + title: 'Create and edit channels', + description: 'Allows members to create new and edit existing channels', + userSurfacedPermission: userSurfacedPermissions.CREATE_AND_EDIT_CHANNELS, +}; +const editThreadName = threadPermissions.EDIT_THREAD_NAME; +const descendantEditThreadName = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.EDIT_THREAD_NAME; +const editThreadDescription = threadPermissions.EDIT_THREAD_DESCRIPTION; +const descendantEditThreadDescription = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.EDIT_THREAD_DESCRIPTION; +const editThreadColor = threadPermissions.EDIT_THREAD_COLOR; +const descendantEditThreadColor = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.EDIT_THREAD_COLOR; +const createSubchannels = threadPermissions.CREATE_SUBCHANNELS; +const descendantCreateSubchannels = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissionFilterPrefixes.TOP_LEVEL + + threadPermissions.CREATE_SUBCHANNELS; +const editThreadAvatar = threadPermissions.EDIT_THREAD_AVATAR; +const descendantEditThreadAvatar = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.EDIT_THREAD_AVATAR; +const descendantTopLevelCreateSidebars = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissionFilterPrefixes.TOP_LEVEL + + threadPermissions.CREATE_SIDEBARS; +const createAndEditChannelsPermissions = new Set([ + editThreadName, + descendantEditThreadName, + editThreadDescription, + descendantEditThreadDescription, + editThreadColor, + descendantEditThreadColor, + createSubchannels, + descendantCreateSubchannels, + editThreadAvatar, + descendantEditThreadAvatar, + descendantTopLevelCreateSidebars, +]); + +const deleteChannelsPermission = { + title: 'Delete channels', + description: 'Allows members to delete channels', + userSurfacedPermission: userSurfacedPermissions.DELETE_CHANNELS, +}; +const deleteThread = threadPermissions.DELETE_THREAD; +const descendantDeleteThread = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.DELETE_THREAD; +const deleteChannelsPermissions = new Set([ + deleteThread, + descendantDeleteThread, +]); + +const addMembersPermission = { + title: 'Add members', + description: 'Allows members to add other members to channels', + userSurfacedPermission: userSurfacedPermissions.ADD_MEMBERS, +}; +const addMembers = threadPermissions.ADD_MEMBERS; +const descendantAddMembers = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.ADD_MEMBERS; +const childOpenAddMembers = + threadPermissionPropagationPrefixes.CHILD + + threadPermissionFilterPrefixes.OPEN + + threadPermissions.ADD_MEMBERS; +const addMembersPermissions = new Set([ + addMembers, + descendantAddMembers, + childOpenAddMembers, +]); + +const removeMembersPermission = { + title: 'Remove members', + description: 'Allows members to remove anybody they can demote from channels', + userSurfacedPermission: userSurfacedPermissions.REMOVE_MEMBERS, +}; +const removeMembers = threadPermissions.REMOVE_MEMBERS; +const descendantRemoveMembers = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.REMOVE_MEMBERS; +const removeMembersPermissions = new Set([ + removeMembers, + descendantRemoveMembers, +]); + +const changeRolePermission = { + title: 'Change roles', + description: 'Allows members to promote and demote other members', + userSurfacedPermission: userSurfacedPermissions.CHANGE_ROLES, +}; +const changeRole = threadPermissions.CHANGE_ROLE; +const descendantChangeRole = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.CHANGE_ROLE; +const changeRolePermissions = new Set([changeRole, descendantChangeRole]); + +const editVisibilityPermission = { + title: 'Edit visibility', + description: 'Allows members to edit visibility permissions of channels', + userSurfacedPermission: userSurfacedPermissions.EDIT_VISIBILITY, +}; +const editPermissions = threadPermissions.EDIT_PERMISSIONS; +const descendantEditPermissions = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.EDIT_PERMISSIONS; +const editVisibilityPermissions = new Set([ + editPermissions, + descendantEditPermissions, +]); + +const managePinsPermission = { + title: 'Manage pins', + description: 'Allows members to pin or unpin messages in channels', + userSurfacedPermission: userSurfacedPermissions.MANAGE_PINS, +}; +const managePins = threadPermissions.MANAGE_PINS; +const descendantManagePins = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.MANAGE_PINS; +const managePinsPermissions = new Set([managePins, descendantManagePins]); + +const reactToMessagePermission = { + title: 'React to messages', + description: 'Allows members to add reactions to messages', + userSurfacedPermission: userSurfacedPermissions.REACT_TO_MESSAGES, +}; +const reactToMessage = threadPermissions.REACT_TO_MESSAGE; +const descendantReactToMessage = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.REACT_TO_MESSAGE; +const reactToMessagePermissions = new Set([ + reactToMessage, + descendantReactToMessage, +]); + +const editMessagePermission = { + title: 'Edit messages', + description: 'Allows members to edit their sent messages', + userSurfacedPermission: userSurfacedPermissions.EDIT_MESSAGES, +}; +const editMessage = threadPermissions.EDIT_MESSAGE; +const descendantEditMessage = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.EDIT_MESSAGE; +const editMessagePermissions = new Set([editMessage, descendantEditMessage]); + +const manageInviteLinksPermission = { + title: 'Manage invite links', + description: 'Allows members to create and delete invite links', + userSurfacedPermission: userSurfacedPermissions.MANAGE_INVITE_LINKS, +}; +const manageInviteLinks = threadPermissions.MANAGE_INVITE_LINKS; +const descendantManageInviteLinks = + threadPermissionPropagationPrefixes.DESCENDANT + + threadPermissions.MANAGE_INVITE_LINKS; +const manageInviteLinksPermissions = new Set([ + manageInviteLinks, + descendantManageInviteLinks, +]); + +export type UserSurfacedPermissionOption = { + +title: string, + +description: string, + +userSurfacedPermission: UserSurfacedPermission, +}; +export const userSurfacedPermissionOptions: $ReadOnlySet = + new Set([ + editCalendarPermission, + knowOfSecretChannelsPermission, + voicedPermission, + createAndEditChannelsPermission, + deleteChannelsPermission, + addMembersPermission, + removeMembersPermission, + changeRolePermission, + editVisibilityPermission, + managePinsPermission, + reactToMessagePermission, + editMessagePermission, + manageInviteLinksPermission, + ]); + +type ConfigurableCommunityPermission = { + +[permission: UserSurfacedPermission]: $ReadOnlySet, +}; +export const configurableCommunityPermissions: ConfigurableCommunityPermission = + Object.freeze({ + [userSurfacedPermissions.EDIT_CALENDAR]: editCalendarPermissions, + [userSurfacedPermissions.KNOW_OF_SECRET_CHANNELS]: + knowOfSecretChannelsPermissions, + [userSurfacedPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS]: + voicedPermissions, + [userSurfacedPermissions.CREATE_AND_EDIT_CHANNELS]: + createAndEditChannelsPermissions, + [userSurfacedPermissions.DELETE_CHANNELS]: deleteChannelsPermissions, + [userSurfacedPermissions.ADD_MEMBERS]: addMembersPermissions, + [userSurfacedPermissions.REMOVE_MEMBERS]: removeMembersPermissions, + [userSurfacedPermissions.CHANGE_ROLES]: changeRolePermissions, + [userSurfacedPermissions.EDIT_VISIBILITY]: editVisibilityPermissions, + [userSurfacedPermissions.MANAGE_PINS]: managePinsPermissions, + [userSurfacedPermissions.REACT_TO_MESSAGES]: reactToMessagePermissions, + [userSurfacedPermissions.EDIT_MESSAGES]: editMessagePermissions, + [userSurfacedPermissions.MANAGE_INVITE_LINKS]: manageInviteLinksPermissions, + }); + export type ThreadPermissionInfo = | { +value: true, +source: string } | { +value: false, +source: null }; diff --git a/lib/utils/role-utils.js b/lib/utils/role-utils.js new file mode 100644 --- /dev/null +++ b/lib/utils/role-utils.js @@ -0,0 +1,38 @@ +// @flow + +import * as React from 'react'; + +import { + type UserSurfacedPermissionOption, + userSurfacedPermissions, + userSurfacedPermissionOptions, +} from '../types/thread-permission-types.js'; +import type { ThreadType } from '../types/thread-types-enum.js'; +import { threadTypes } from '../types/thread-types-enum.js'; + +function useFilterPermissionOptionsByThreadType( + threadType: ThreadType, +): $ReadOnlySet { + // If the thread is a community announcement root, we want to allow + // the option to be voiced in the announcement channels. Otherwise, + // we want to remove that option from being configured since this will + // be guaranteed on the keyserver. + const shouldFilterVoicedInAnnouncementChannel = + threadType === threadTypes.COMMUNITY_ROOT; + + return React.useMemo(() => { + if (!shouldFilterVoicedInAnnouncementChannel) { + return userSurfacedPermissionOptions; + } + + return new Set( + [...userSurfacedPermissionOptions].filter( + option => + option.userSurfacedPermission !== + userSurfacedPermissions.VOICED_IN_ANNOUNCEMENT_CHANNELS, + ), + ); + }, [shouldFilterVoicedInAnnouncementChannel]); +} + +export { useFilterPermissionOptionsByThreadType }; diff --git a/native/roles/create-roles-screen.react.js b/native/roles/create-roles-screen.react.js --- a/native/roles/create-roles-screen.react.js +++ b/native/roles/create-roles-screen.react.js @@ -1,11 +1,22 @@ // @flow import * as React from 'react'; +import { View, Text, TouchableOpacity } from 'react-native'; +import { ScrollView } from 'react-native-gesture-handler'; +import { + type UserSurfacedPermissionOption, + type UserSurfacedPermission, +} from 'lib/types/thread-permission-types.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; +import { useFilterPermissionOptionsByThreadType } from 'lib/utils/role-utils.js'; import type { RolesNavigationProp } from './roles-navigator.react.js'; +import EnumSettingsOption from '../components/enum-settings-option.react.js'; +import SWMansionIcon from '../components/swmansion-icon.react.js'; +import TextInput from '../components/text-input.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; +import { useStyles } from '../themes/colors.js'; export type CreateRolesScreenParams = { +threadInfo: ThreadInfo, @@ -17,9 +28,168 @@ +route: NavigationRoute<'CreateRolesScreen'>, }; -// eslint-disable-next-line no-unused-vars function CreateRolesScreen(props: CreateRolesScreenProps): React.Node { - return <>; + // eslint-disable-next-line no-unused-vars + const { threadInfo, action } = props.route.params; + + const [customRoleName, setCustomRoleName] = + React.useState('New role'); + const [selectedPermissions, setSelectedPermissions] = React.useState< + $ReadOnlySet, + >(new Set()); + + const styles = useStyles(unboundStyles); + + const onClearPermissions = React.useCallback(() => { + setSelectedPermissions(new Set()); + }, []); + + const isSelectedPermissionsEmpty = selectedPermissions.size === 0; + const clearPermissionsText = React.useMemo(() => { + const textStyle = isSelectedPermissionsEmpty + ? styles.clearPermissionsTextDisabled + : styles.clearPermissionsText; + + return ( + + Clear permissions + + ); + }, [ + isSelectedPermissionsEmpty, + onClearPermissions, + styles.clearPermissionsText, + styles.clearPermissionsTextDisabled, + ]); + + const isUserSurfacedPermissionSelected = React.useCallback( + (option: UserSurfacedPermissionOption) => + selectedPermissions.has(option.userSurfacedPermission), + [selectedPermissions], + ); + + const onEnumValuePress = React.useCallback( + (option: UserSurfacedPermissionOption) => + setSelectedPermissions(currentPermissions => { + if (currentPermissions.has(option.userSurfacedPermission)) { + const newPermissions = new Set(currentPermissions); + newPermissions.delete(option.userSurfacedPermission); + return newPermissions; + } else { + return new Set([ + ...currentPermissions, + option.userSurfacedPermission, + ]); + } + }), + [], + ); + + const filteredUserSurfacedPermissionOptions = + useFilterPermissionOptionsByThreadType(threadInfo.type); + + const permissionsList = React.useMemo( + () => + [...filteredUserSurfacedPermissionOptions].map(permission => ( + onEnumValuePress(permission)} + /> + )), + [ + isUserSurfacedPermissionSelected, + filteredUserSurfacedPermissionOptions, + onEnumValuePress, + ], + ); + + const onChangeRoleNameInput = React.useCallback((roleName: string) => { + setCustomRoleName(roleName); + }, []); + + return ( + + + ROLE NAME + + + + + + + + PERMISSIONS + {clearPermissionsText} + + + {permissionsList} + + + + ); } +const unboundStyles = { + roleNameContainer: { + marginTop: 30, + }, + roleNameText: { + color: 'panelBackgroundLabel', + fontSize: 12, + marginBottom: 5, + marginLeft: 10, + }, + roleInput: { + backgroundColor: 'panelForeground', + padding: 12, + flexDirection: 'row', + justifyContent: 'space-between', + }, + roleInputComponent: { + color: 'panelForegroundLabel', + fontSize: 16, + }, + pencilIcon: { + color: 'panelInputSecondaryForeground', + }, + permissionsContainer: { + marginTop: 20, + paddingBottom: 220, + }, + permissionsHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + permissionsText: { + color: 'panelBackgroundLabel', + fontSize: 12, + marginLeft: 10, + }, + clearPermissionsText: { + color: 'purpleLink', + fontSize: 12, + marginRight: 15, + }, + clearPermissionsTextDisabled: { + color: 'disabledButton', + fontSize: 12, + marginRight: 15, + }, + permissionsListContainer: { + backgroundColor: 'panelForeground', + marginTop: 10, + }, +}; + export default CreateRolesScreen;