Skip to content

Commit

Permalink
[lib] Set roles and permissions in thick threads based on parent perm…
Browse files Browse the repository at this point in the history
…issions

Summary:
When we create a thick thread, we should check if the viewer is a member and set the role and permissions accordingly. This case can happen when we receive a create sidebar operation.

When a user leaves a sidebar, we should also update their permissions.

https://linear.app/comm/issue/ENG-9318/sidebar-joining-issues

Test Plan:
Create a thick sidebar without the viewer and check if the join button is visible. Join and leave the sidebar - the button should reappear.
Created a new sidebar in a thread with three users and added one of them. Checked if the added user can send messages and if the second user can join the sidebar.

Reviewers: kamil, ashoat

Reviewed By: ashoat

Differential Revision: https://phab.comm.dev/D13418
  • Loading branch information
palys-swm committed Sep 25, 2024
1 parent 1bab181 commit 1d2bb3b
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 14 deletions.
10 changes: 10 additions & 0 deletions lib/permissions/thread-permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,10 @@ function getThickThreadRolePermissionsBlob(
threadType: ThickThreadType,
): ThreadRolePermissionsBlob {
invariant(threadTypeIsThick(threadType), 'ThreadType should be thick');
const openDescendantKnowOf = OPEN_DESCENDANT + threadPermissions.KNOW_OF;
const openDescendantVisible = OPEN_DESCENDANT + threadPermissions.VISIBLE;
const openChildJoinThread = OPEN_CHILD + threadPermissions.JOIN_THREAD;

const basePermissions = {
[threadPermissions.KNOW_OF]: true,
[threadPermissions.VISIBLE]: true,
Expand Down Expand Up @@ -493,6 +497,9 @@ function getThickThreadRolePermissionsBlob(
...basePermissions,
[threadPermissions.EDIT_ENTRIES]: true,
[threadPermissions.CREATE_SIDEBARS]: true,
[openDescendantKnowOf]: true,
[openDescendantVisible]: true,
[openChildJoinThread]: true,
};
}
return {
Expand All @@ -501,6 +508,9 @@ function getThickThreadRolePermissionsBlob(
[threadPermissions.CREATE_SIDEBARS]: true,
[threadPermissions.ADD_MEMBERS]: true,
[threadPermissions.LEAVE_THREAD]: true,
[openDescendantKnowOf]: true,
[openDescendantVisible]: true,
[openChildJoinThread]: true,
};
}

Expand Down
17 changes: 17 additions & 0 deletions lib/shared/dm-ops/add-members-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,27 @@ const addMembersSpec: DMOperationSpec<DMAddMembersOperation> = Object.freeze({
roleIsDefaultRole(role),
)?.id;
invariant(defaultRoleID, 'Default role ID must exist');

const parentThreadID = currentThreadInfo.parentThreadID;
const parentThreadInfo = parentThreadID
? utilities.threadInfos[parentThreadID]
: null;
if (parentThreadID && !parentThreadInfo) {
console.log(
`Parent thread with ID ${parentThreadID} was expected while adding ` +
'thread members but is missing from the store',
);
}
invariant(
!parentThreadInfo || parentThreadInfo.thick,
'Parent thread should be thick',
);

const { membershipPermissions } = createRoleAndPermissionForThickThreads(
currentThreadInfo.type,
currentThreadInfo.id,
defaultRoleID,
parentThreadInfo,
);

const memberTimestamps = { ...currentThreadInfo.timestamps.members };
Expand Down
4 changes: 2 additions & 2 deletions lib/shared/dm-ops/add-viewer-to-thread-members-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const addViewerToThreadMembersSpec: DMOperationSpec<DMAddViewerToThreadMembersOp
) => {
const { time, messageID, addedUserIDs, existingThreadDetails } =
dmOperation;
const { viewerID, threadInfos } = utilities;
const { threadInfos } = utilities;

const { rawMessageInfo } =
createAddViewerToThreadMembersMessageDataWithInfoFromDMOp(dmOperation);
Expand Down Expand Up @@ -115,7 +115,7 @@ const addViewerToThreadMembersSpec: DMOperationSpec<DMAddViewerToThreadMembersOp
},
},
},
viewerID,
utilities,
);
const updateInfos = [
{
Expand Down
2 changes: 1 addition & 1 deletion lib/shared/dm-ops/create-sidebar-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ const createSidebarSpec: DMOperationSpec<DMCreateSidebarOperation> =
containingThreadID: parentThreadID,
timestamps: createThreadTimestamps(time, allMemberIDs),
},
viewerID,
utilities,
);

const { sidebarSourceMessageInfo, createSidebarMessageInfo } =
Expand Down
98 changes: 88 additions & 10 deletions lib/shared/dm-ops/create-thread-spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow

import invariant from 'invariant';
import uuid from 'uuid';

import type {
Expand All @@ -11,6 +12,7 @@ import {
getAllThreadPermissions,
makePermissionsBlob,
getThickThreadRolePermissionsBlob,
makePermissionsForChildrenBlob,
} from '../../permissions/thread-permissions.js';
import type {
CreateThickRawThreadInfoInput,
Expand All @@ -34,16 +36,58 @@ import { generatePendingThreadColor } from '../color-utils.js';
import { rawMessageInfoFromMessageData } from '../message-utils.js';
import { createThreadTimestamps } from '../thread-utils.js';

function createPermissionsInfo(
threadID: string,
threadType: ThickThreadType,
isMember: boolean,
parentThreadInfo: ?ThickRawThreadInfo,
): ThreadPermissionsInfo {
let rolePermissions = null;
if (isMember) {
rolePermissions = getThickThreadRolePermissionsBlob(threadType);
}

let permissionsFromParent = null;
if (parentThreadInfo) {
const parentThreadRolePermissions = getThickThreadRolePermissionsBlob(
parentThreadInfo.type,
);
const parentPermissionsBlob = makePermissionsBlob(
parentThreadRolePermissions,
null,
parentThreadInfo.id,
parentThreadInfo.type,
);
permissionsFromParent = makePermissionsForChildrenBlob(
parentPermissionsBlob,
);
}

return getAllThreadPermissions(
makePermissionsBlob(
rolePermissions,
permissionsFromParent,
threadID,
threadType,
),
threadID,
);
}

function createRoleAndPermissionForThickThreads(
threadType: ThickThreadType,
threadID: string,
roleID: string,
parentThreadInfo: ?ThickRawThreadInfo,
): { +role: RoleInfo, +membershipPermissions: ThreadPermissionsInfo } {
const rolePermissions = getThickThreadRolePermissionsBlob(threadType);
const membershipPermissions = getAllThreadPermissions(
makePermissionsBlob(rolePermissions, null, threadID, threadType),
const membershipPermissions = createPermissionsInfo(
threadID,
threadType,
true,
parentThreadInfo,
);

const role: RoleInfo = {
...minimallyEncodeRoleInfo({
id: roleID,
Expand All @@ -62,7 +106,7 @@ function createRoleAndPermissionForThickThreads(
type MutableThickRawThreadInfo = { ...ThickRawThreadInfo };
function createThickRawThreadInfo(
input: CreateThickRawThreadInfoInput,
viewerID: string,
utilities: ProcessDMOperationUtilities,
): MutableThickRawThreadInfo {
const {
threadID,
Expand All @@ -86,8 +130,38 @@ function createThickRawThreadInfo(
const memberIDs = allMemberIDsWithSubscriptions.map(({ id }) => id);
const threadColor = color ?? generatePendingThreadColor(memberIDs);

const parentThreadInfo = parentThreadID
? utilities.threadInfos[parentThreadID]
: null;
if (parentThreadID && !parentThreadInfo) {
console.log(
`Parent thread with ID ${parentThreadID} was expected while creating ` +
'thick thread but is missing from the store',
);
}
invariant(
!parentThreadInfo || parentThreadInfo.thick,
'Parent thread should be thick',
);

const { membershipPermissions, role } =
createRoleAndPermissionForThickThreads(threadType, threadID, roleID);
createRoleAndPermissionForThickThreads(
threadType,
threadID,
roleID,
parentThreadInfo,
);

const viewerIsMember = allMemberIDsWithSubscriptions.some(
member => member.id === utilities.viewerID,
);
const viewerRoleID = viewerIsMember ? role.id : null;
const viewerMembershipPermissions = createPermissionsInfo(
threadID,
threadType,
viewerIsMember,
parentThreadInfo,
);

const newThread: MutableThickRawThreadInfo = {
thick: true,
Expand All @@ -101,18 +175,21 @@ function createThickRawThreadInfo(
({ id: memberID, subscription }) =>
minimallyEncodeMemberInfo<ThickMemberInfo>({
id: memberID,
role: role.id,
permissions: membershipPermissions,
isSender: memberID === viewerID,
role: memberID === utilities.viewerID ? viewerRoleID : role.id,
permissions:
memberID === utilities.viewerID
? viewerMembershipPermissions
: membershipPermissions,
isSender: memberID === utilities.viewerID,
subscription,
}),
),
roles: {
[role.id]: role,
},
currentUser: minimallyEncodeThreadCurrentUserInfo({
role: role.id,
permissions: membershipPermissions,
role: viewerRoleID,
permissions: viewerMembershipPermissions,
subscription: joinThreadSubscription,
unread,
}),
Expand Down Expand Up @@ -189,7 +266,7 @@ const createThreadSpec: DMOperationSpec<DMCreateThreadOperation> =
unread: creatorID !== viewerID,
timestamps: createThreadTimestamps(time, allMemberIDs),
},
viewerID,
utilities,
);

const { rawMessageInfo } =
Expand Down Expand Up @@ -222,4 +299,5 @@ export {
createThickRawThreadInfo,
createThreadSpec,
createRoleAndPermissionForThickThreads,
createPermissionsInfo,
};
19 changes: 18 additions & 1 deletion lib/shared/dm-ops/join-thread-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const joinThreadSpec: DMOperationSpec<DMJoinThreadOperation> = Object.freeze({
members: memberTimestamps,
},
},
viewerID,
utilities,
);
updateInfos.push({
type: updateTypes.JOIN_THREAD,
Expand All @@ -135,10 +135,27 @@ const joinThreadSpec: DMOperationSpec<DMJoinThreadOperation> = Object.freeze({
roleIsDefaultRole(role),
)?.id;
invariant(defaultRoleID, 'Default role ID must exist');

const parentThreadID = existingThreadDetails.parentThreadID;
const parentThreadInfo = parentThreadID
? utilities.threadInfos[parentThreadID]
: null;
if (parentThreadID && !parentThreadInfo) {
console.log(
`Parent thread with ID ${parentThreadID} was expected while joining ` +
'thick thread but is missing from the store',
);
}
invariant(
!parentThreadInfo || parentThreadInfo.thick,
'Parent thread should be thick',
);

const { membershipPermissions } = createRoleAndPermissionForThickThreads(
currentThreadInfo.type,
currentThreadInfo.id,
defaultRoleID,
parentThreadInfo,
);

const member = minimallyEncodeMemberInfo<ThickMemberInfo>({
Expand Down
34 changes: 34 additions & 0 deletions lib/shared/dm-ops/leave-thread-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import invariant from 'invariant';
import uuid from 'uuid';

import { createPermissionsInfo } from './create-thread-spec.js';
import type {
DMOperationSpec,
ProcessDMOperationUtilities,
} from './dm-op-spec.js';
import type { DMLeaveThreadOperation } from '../../types/dm-ops.js';
import { messageTypes } from '../../types/message-types-enum.js';
import type { ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js';
import { minimallyEncodeThreadCurrentUserInfo } from '../../types/minimally-encoded-thread-permissions-types.js';
import { threadTypes } from '../../types/thread-types-enum.js';
import type { RawThreadInfos } from '../../types/thread-types.js';
import { updateTypes } from '../../types/update-types-enum.js';
Expand Down Expand Up @@ -121,9 +123,41 @@ const leaveThreadSpec: DMOperationSpec<DMLeaveThreadOperation> = Object.freeze({
...memberTimestamps[editorID],
isMember: time,
};

let currentUser = threadInfo.currentUser;
if (editorID === viewerID) {
const parentThreadID = threadInfo.parentThreadID;
const parentThreadInfo = parentThreadID
? utilities.threadInfos[parentThreadID]
: null;
if (parentThreadID && !parentThreadInfo) {
console.log(
`Parent thread with ID ${parentThreadID} was expected while ` +
'leaving a thread but is missing from the store',
);
}
invariant(
parentThreadInfo?.thick,
'Parent thread should be present and thick',
);
const viewerMembershipPermissions = createPermissionsInfo(
threadID,
threadInfo.type,
false,
parentThreadInfo,
);
const { minimallyEncoded, permissions, ...currentUserInfo } = currentUser;
currentUser = minimallyEncodeThreadCurrentUserInfo({
...currentUserInfo,
role: null,
permissions: viewerMembershipPermissions,
});
}

const updatedThreadInfo = {
...threadInfo,
members: threadInfo.members.filter(member => member.id !== editorID),
currentUser,
timestamps: {
...threadInfo.timestamps,
members: memberTimestamps,
Expand Down

0 comments on commit 1d2bb3b

Please sign in to comment.