diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js index 0e5ae4f71..17ac5c4b3 100644 --- a/lib/types/thread-types.js +++ b/lib/types/thread-types.js @@ -1,379 +1,381 @@ // @flow import type { ThreadSubscription } from './subscription-types'; import type { RawMessageInfo, MessageTruncationStatuses, } from './message-types'; import type { UserInfo, AccountUserInfo } from './user-types'; import type { CalendarQuery, CalendarResult, RawEntryInfo, } from './entry-types'; import type { UpdateInfo } from './update-types'; import type { ClientThreadInconsistencyReportCreationRequest } from './report-types'; import PropTypes from 'prop-types'; import invariant from 'invariant'; export const threadTypes = Object.freeze({ //OPEN: 0, (DEPRECATED) //CLOSED: 1, (DEPRECATED) //SECRET: 2, (DEPRECATED) CHAT_NESTED_OPEN: 3, CHAT_SECRET: 4, SIDEBAR: 5, PERSONAL: 6, }); export type ThreadType = $Values; export function assertThreadType(threadType: number): ThreadType { invariant( threadType === 3 || threadType === 4 || threadType === 5 || threadType === 6, 'number is not ThreadType enum', ); return threadType; } export const threadPermissions = Object.freeze({ KNOW_OF: 'know_of', VISIBLE: 'visible', VOICED: 'voiced', EDIT_ENTRIES: 'edit_entries', EDIT_THREAD: 'edit_thread', DELETE_THREAD: 'delete_thread', CREATE_SUBTHREADS: 'create_subthreads', CREATE_SIDEBARS: 'create_sidebars', JOIN_THREAD: 'join_thread', EDIT_PERMISSIONS: 'edit_permissions', ADD_MEMBERS: 'add_members', REMOVE_MEMBERS: 'remove_members', CHANGE_ROLE: 'change_role', + LEAVE_THREAD: 'leave_thread', }); export type ThreadPermission = $Values; export function assertThreadPermissions( ourThreadPermissions: string, ): ThreadPermission { invariant( ourThreadPermissions === 'know_of' || ourThreadPermissions === 'visible' || ourThreadPermissions === 'voiced' || ourThreadPermissions === 'edit_entries' || ourThreadPermissions === 'edit_thread' || ourThreadPermissions === 'delete_thread' || ourThreadPermissions === 'create_subthreads' || ourThreadPermissions === 'create_sidebars' || ourThreadPermissions === 'join_thread' || ourThreadPermissions === 'edit_permissions' || ourThreadPermissions === 'add_members' || ourThreadPermissions === 'remove_members' || - ourThreadPermissions === 'change_role', + ourThreadPermissions === 'change_role' || + ourThreadPermissions === 'leave_thread', 'string is not threadPermissions enum', ); return ourThreadPermissions; } export const threadPermissionPrefixes = Object.freeze({ DESCENDANT: 'descendant_', CHILD: 'child_', OPEN: 'open_', OPEN_DESCENDANT: 'descendant_open_', }); export type ThreadPermissionInfo = | {| value: true, source: string |} | {| value: false, source: null |}; export type ThreadPermissionsBlob = { [permission: string]: ThreadPermissionInfo, }; export type ThreadRolePermissionsBlob = { [permission: string]: boolean }; export type ThreadPermissionsInfo = { [permission: ThreadPermission]: ThreadPermissionInfo, }; export const threadPermissionsInfoPropType = PropTypes.objectOf( PropTypes.oneOfType([ PropTypes.shape({ value: PropTypes.oneOf([true]), source: PropTypes.string.isRequired, }), PropTypes.shape({ value: PropTypes.oneOf([false]), source: PropTypes.oneOf([null]), }), ]), ); export type MemberInfo = {| id: string, role: ?string, permissions: ThreadPermissionsInfo, |}; export const memberInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, role: PropTypes.string, permissions: threadPermissionsInfoPropType.isRequired, }); export type RelativeMemberInfo = {| ...MemberInfo, username: ?string, isViewer: boolean, |}; export const relativeMemberInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, role: PropTypes.string, permissions: threadPermissionsInfoPropType.isRequired, username: PropTypes.string, isViewer: PropTypes.bool.isRequired, }); export type RoleInfo = {| id: string, name: string, permissions: ThreadRolePermissionsBlob, isDefault: boolean, |}; export type ThreadCurrentUserInfo = {| role: ?string, permissions: ThreadPermissionsInfo, subscription: ThreadSubscription, unread: ?boolean, |}; export type RawThreadInfo = {| id: string, type: ThreadType, name: ?string, description: ?string, color: string, // hex, without "#" or "0x" creationTime: number, // millisecond timestamp parentThreadID: ?string, members: $ReadOnlyArray, roles: { [id: string]: RoleInfo }, currentUser: ThreadCurrentUserInfo, |}; export type ThreadInfo = {| id: string, type: ThreadType, name: ?string, uiName: string, description: ?string, color: string, // hex, without "#" or "0x" creationTime: number, // millisecond timestamp parentThreadID: ?string, members: $ReadOnlyArray, roles: { [id: string]: RoleInfo }, currentUser: ThreadCurrentUserInfo, |}; export const threadTypePropType = PropTypes.oneOf([ threadTypes.CHAT_NESTED_OPEN, threadTypes.CHAT_SECRET, threadTypes.SIDEBAR, threadTypes.PERSONAL, ]); const rolePropType = PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, permissions: PropTypes.objectOf(PropTypes.bool).isRequired, isDefault: PropTypes.bool.isRequired, }); const currentUserPropType = PropTypes.shape({ role: PropTypes.string, permissions: threadPermissionsInfoPropType.isRequired, subscription: PropTypes.shape({ pushNotifs: PropTypes.bool.isRequired, home: PropTypes.bool.isRequired, }).isRequired, unread: PropTypes.bool, }); export const rawThreadInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, type: threadTypePropType.isRequired, name: PropTypes.string, description: PropTypes.string, color: PropTypes.string.isRequired, creationTime: PropTypes.number.isRequired, parentThreadID: PropTypes.string, members: PropTypes.arrayOf(memberInfoPropType).isRequired, roles: PropTypes.objectOf(rolePropType).isRequired, currentUser: currentUserPropType.isRequired, }); export const threadInfoPropType = PropTypes.shape({ id: PropTypes.string.isRequired, type: threadTypePropType.isRequired, name: PropTypes.string, uiName: PropTypes.string.isRequired, description: PropTypes.string, color: PropTypes.string.isRequired, creationTime: PropTypes.number.isRequired, parentThreadID: PropTypes.string, members: PropTypes.arrayOf(memberInfoPropType).isRequired, roles: PropTypes.objectOf(rolePropType).isRequired, currentUser: currentUserPropType.isRequired, }); export type ServerMemberInfo = {| id: string, role: ?string, permissions: ThreadPermissionsInfo, subscription: ThreadSubscription, unread: ?boolean, |}; export type ServerThreadInfo = {| id: string, type: ThreadType, name: ?string, description: ?string, color: string, // hex, without "#" or "0x" creationTime: number, // millisecond timestamp parentThreadID: ?string, members: $ReadOnlyArray, roles: { [id: string]: RoleInfo }, |}; export type ThreadStore = {| threadInfos: { [id: string]: RawThreadInfo }, inconsistencyReports: $ReadOnlyArray, |}; export type ThreadDeletionRequest = {| threadID: string, accountPassword: string, |}; export type RemoveMembersRequest = {| threadID: string, memberIDs: $ReadOnlyArray, |}; export type RoleChangeRequest = {| threadID: string, memberIDs: $ReadOnlyArray, role: string, |}; export type ChangeThreadSettingsResult = {| threadInfo?: RawThreadInfo, threadInfos?: { [id: string]: RawThreadInfo }, updatesResult: { newUpdates: $ReadOnlyArray, }, newMessageInfos: $ReadOnlyArray, |}; export type ChangeThreadSettingsPayload = {| threadID: string, updatesResult: { newUpdates: $ReadOnlyArray, }, newMessageInfos: $ReadOnlyArray, |}; export type LeaveThreadRequest = {| threadID: string, |}; export type LeaveThreadResult = {| threadInfos?: { [id: string]: RawThreadInfo }, updatesResult: { newUpdates: $ReadOnlyArray, }, |}; export type LeaveThreadPayload = {| updatesResult: { newUpdates: $ReadOnlyArray, }, |}; export type ThreadChanges = $Shape<{| type: ThreadType, name: string, description: string, color: string, parentThreadID: string, newMemberIDs: $ReadOnlyArray, |}>; export type UpdateThreadRequest = {| threadID: string, changes: ThreadChanges, |}; export type NewThreadRequest = {| type: ThreadType, name?: ?string, description?: ?string, color?: ?string, parentThreadID?: ?string, initialMemberIDs?: ?$ReadOnlyArray, |}; export type NewThreadResponse = {| updatesResult: { newUpdates: $ReadOnlyArray, }, newMessageInfos: $ReadOnlyArray, newThreadInfo?: RawThreadInfo, newThreadID?: string, |}; export type NewThreadResult = {| updatesResult: { newUpdates: $ReadOnlyArray, }, newMessageInfos: $ReadOnlyArray, newThreadID: string, |}; export type ServerThreadJoinRequest = {| threadID: string, calendarQuery?: ?CalendarQuery, |}; export type ClientThreadJoinRequest = {| threadID: string, calendarQuery: CalendarQuery, |}; export type ThreadJoinResult = {| threadInfos?: { [id: string]: RawThreadInfo }, updatesResult: { newUpdates: $ReadOnlyArray, }, rawMessageInfos: $ReadOnlyArray, truncationStatuses: MessageTruncationStatuses, userInfos: { [string]: AccountUserInfo }, rawEntryInfos?: ?$ReadOnlyArray, |}; export type ThreadJoinPayload = {| updatesResult: { newUpdates: $ReadOnlyArray, }, rawMessageInfos: RawMessageInfo[], truncationStatuses: MessageTruncationStatuses, userInfos: $ReadOnlyArray, calendarResult: CalendarResult, |}; export type SidebarInfo = {| +threadInfo: ThreadInfo, +lastUpdatedTime: number, +mostRecentNonLocalMessage: ?string, |}; // We can show a max of 3 sidebars inline underneath their parent in the chat // tab. If there are more, we show a button that opens a modal to see the rest export const maxReadSidebars = 3; // We can show a max of 5 sidebars inline underneath their parent // in the chat tab if every one of the displayed sidebars is unread export const maxUnreadSidebars = 5; diff --git a/server/src/creators/role-creator.js b/server/src/creators/role-creator.js index de694b205..cd3928fa9 100644 --- a/server/src/creators/role-creator.js +++ b/server/src/creators/role-creator.js @@ -1,219 +1,223 @@ // @flow import { type RoleInfo, threadPermissions, threadPermissionPrefixes, type ThreadRolePermissionsBlob, type ThreadType, threadTypes, } from 'lib/types/thread-types'; import { dbQuery, SQL } from '../database/database'; import createIDs from './id-creator'; type InitialRoles = {| default: RoleInfo, creator: RoleInfo, |}; async function createInitialRolesForNewThread( threadID: string, threadType: ThreadType, ): Promise { const rolePermissions = getRolePermissionBlobsForChat(threadType); const ids = await createIDs('roles', Object.values(rolePermissions).length); const time = Date.now(); const newRows = []; const namesToIDs = {}; for (const name in rolePermissions) { const id = ids.shift(); namesToIDs[name] = id; const permissionsBlob = JSON.stringify(rolePermissions[name]); newRows.push([id, threadID, name, permissionsBlob, time]); } const query = SQL` INSERT INTO roles (id, thread, name, permissions, creation_time) VALUES ${newRows} `; await dbQuery(query); const defaultRoleInfo = { id: namesToIDs.Members, name: 'Members', permissions: rolePermissions.Members, isDefault: true, }; if (!rolePermissions.Admins) { return { default: defaultRoleInfo, creator: defaultRoleInfo, }; } const adminRoleInfo = { id: namesToIDs.Admins, name: 'Admins', permissions: rolePermissions.Admins, isDefault: false, }; return { default: defaultRoleInfo, creator: adminRoleInfo, }; } type RolePermissionBlobs = {| Members: ThreadRolePermissionsBlob, Admins?: ThreadRolePermissionsBlob, |}; // Originally all chat threads were orgs, but for the alpha launch I decided // it's better to keep it simple. I'll probably reintroduce orgs at some point. // eslint-disable-next-line no-unused-vars function getRolePermissionBlobsForOrg(): RolePermissionBlobs { const openDescendantKnowOf = threadPermissionPrefixes.OPEN_DESCENDANT + threadPermissions.KNOW_OF; const openDescendantVisible = threadPermissionPrefixes.OPEN_DESCENDANT + threadPermissions.VISIBLE; const openDescendantJoinThread = threadPermissionPrefixes.OPEN_DESCENDANT + threadPermissions.JOIN_THREAD; const memberPermissions = { [threadPermissions.KNOW_OF]: true, [threadPermissions.VISIBLE]: true, [threadPermissions.JOIN_THREAD]: true, [openDescendantKnowOf]: true, [openDescendantVisible]: true, [openDescendantJoinThread]: true, [threadPermissions.VOICED]: true, [threadPermissions.EDIT_ENTRIES]: true, [threadPermissions.EDIT_THREAD]: true, [threadPermissions.CREATE_SUBTHREADS]: true, [threadPermissions.CREATE_SIDEBARS]: true, [threadPermissions.ADD_MEMBERS]: true, + [threadPermissions.LEAVE_THREAD]: true, }; const descendantKnowOf = threadPermissionPrefixes.DESCENDANT + threadPermissions.KNOW_OF; const descendantVisible = threadPermissionPrefixes.DESCENDANT + threadPermissions.VISIBLE; const descendantJoinThread = threadPermissionPrefixes.DESCENDANT + threadPermissions.JOIN_THREAD; const descendantVoiced = threadPermissionPrefixes.DESCENDANT + threadPermissions.VOICED; const descendantEditEntries = threadPermissionPrefixes.DESCENDANT + threadPermissions.EDIT_ENTRIES; const descendantEditThread = threadPermissionPrefixes.DESCENDANT + threadPermissions.EDIT_THREAD; const descendantCreateSubthreads = threadPermissionPrefixes.DESCENDANT + threadPermissions.CREATE_SUBTHREADS; const descendantCreateSidebars = threadPermissionPrefixes.DESCENDANT + threadPermissions.CREATE_SIDEBARS; const descendantAddMembers = threadPermissionPrefixes.DESCENDANT + threadPermissions.ADD_MEMBERS; const descendantDeleteThread = threadPermissionPrefixes.DESCENDANT + threadPermissions.DELETE_THREAD; const descendantEditPermissions = threadPermissionPrefixes.DESCENDANT + threadPermissions.EDIT_PERMISSIONS; const descendantRemoveMembers = threadPermissionPrefixes.DESCENDANT + threadPermissions.REMOVE_MEMBERS; const descendantChangeRole = threadPermissionPrefixes.DESCENDANT + threadPermissions.CHANGE_ROLE; const adminPermissions = { [threadPermissions.KNOW_OF]: true, [threadPermissions.VISIBLE]: true, [threadPermissions.JOIN_THREAD]: true, [threadPermissions.VOICED]: true, [threadPermissions.EDIT_ENTRIES]: true, [threadPermissions.EDIT_THREAD]: true, [threadPermissions.CREATE_SUBTHREADS]: true, [threadPermissions.CREATE_SIDEBARS]: true, [threadPermissions.ADD_MEMBERS]: true, [threadPermissions.DELETE_THREAD]: true, [threadPermissions.EDIT_PERMISSIONS]: true, [threadPermissions.REMOVE_MEMBERS]: true, [threadPermissions.CHANGE_ROLE]: true, + [threadPermissions.LEAVE_THREAD]: true, [descendantKnowOf]: true, [descendantVisible]: true, [descendantJoinThread]: true, [descendantVoiced]: true, [descendantEditEntries]: true, [descendantEditThread]: true, [descendantCreateSubthreads]: true, [descendantCreateSidebars]: true, [descendantAddMembers]: true, [descendantDeleteThread]: true, [descendantEditPermissions]: true, [descendantRemoveMembers]: true, [descendantChangeRole]: true, }; return { Members: memberPermissions, Admins: adminPermissions, }; } function getRolePermissionBlobsForChat( threadType: ThreadType, ): RolePermissionBlobs { if (threadType === threadTypes.SIDEBAR) { const memberPermissions = { [threadPermissions.KNOW_OF]: true, [threadPermissions.VISIBLE]: true, [threadPermissions.VOICED]: true, [threadPermissions.EDIT_THREAD]: true, [threadPermissions.ADD_MEMBERS]: true, [threadPermissions.EDIT_PERMISSIONS]: true, [threadPermissions.REMOVE_MEMBERS]: true, + [threadPermissions.LEAVE_THREAD]: true, }; return { Members: memberPermissions, }; } const openDescendantKnowOf = threadPermissionPrefixes.OPEN_DESCENDANT + threadPermissions.KNOW_OF; const openDescendantVisible = threadPermissionPrefixes.OPEN_DESCENDANT + threadPermissions.VISIBLE; const openDescendantJoinThread = threadPermissionPrefixes.OPEN_DESCENDANT + threadPermissions.JOIN_THREAD; if (threadType === threadTypes.PERSONAL) { return { Members: { [threadPermissions.KNOW_OF]: true, [threadPermissions.VISIBLE]: true, [threadPermissions.VOICED]: true, [threadPermissions.EDIT_ENTRIES]: true, [threadPermissions.EDIT_THREAD]: true, [threadPermissions.CREATE_SUBTHREADS]: true, [threadPermissions.CREATE_SIDEBARS]: true, [openDescendantKnowOf]: true, [openDescendantVisible]: true, [openDescendantJoinThread]: true, }, }; } const memberPermissions = { [threadPermissions.KNOW_OF]: true, [threadPermissions.VISIBLE]: true, [threadPermissions.JOIN_THREAD]: true, [threadPermissions.VOICED]: true, [threadPermissions.EDIT_ENTRIES]: true, [threadPermissions.EDIT_THREAD]: true, [threadPermissions.CREATE_SUBTHREADS]: true, [threadPermissions.CREATE_SIDEBARS]: true, [threadPermissions.ADD_MEMBERS]: true, [threadPermissions.EDIT_PERMISSIONS]: true, [threadPermissions.REMOVE_MEMBERS]: true, + [threadPermissions.LEAVE_THREAD]: true, [openDescendantKnowOf]: true, [openDescendantVisible]: true, [openDescendantJoinThread]: true, }; return { Members: memberPermissions, }; } export { createInitialRolesForNewThread, getRolePermissionBlobsForChat };