Page MenuHomePhorge

D8574.1765179220.diff
No OneTemporary

Size
14 KB
Referenced Files
None
Subscribers
None

D8574.1765179220.diff

diff --git a/keyserver/src/deleters/role-deleters.js b/keyserver/src/deleters/role-deleters.js
--- a/keyserver/src/deleters/role-deleters.js
+++ b/keyserver/src/deleters/role-deleters.js
@@ -1,6 +1,22 @@
// @flow
+import { threadPermissions } from 'lib/types/thread-permission-types.js';
+import type {
+ RoleDeletionRequest,
+ RoleDeletionResult,
+} from 'lib/types/thread-types.js';
+import { updateTypes } from 'lib/types/update-types-enum.js';
+import { ServerError } from 'lib/utils/errors.js';
+
+import { createUpdates } from '../creators/update-creator.js';
import { dbQuery, SQL } from '../database/database.js';
+import {
+ fetchServerThreadInfos,
+ rawThreadInfosFromServerThreadInfos,
+} from '../fetchers/thread-fetchers.js';
+import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js';
+import type { Viewer } from '../session/viewer.js';
+import { updateRole } from '../updaters/thread-updaters.js';
async function deleteOrphanedRoles(): Promise<void> {
await dbQuery(SQL`
@@ -12,4 +28,96 @@
`);
}
-export { deleteOrphanedRoles };
+async function deleteRole(
+ viewer: Viewer,
+ request: RoleDeletionRequest,
+): Promise<RoleDeletionResult> {
+ const hasPermission = checkThreadPermission(
+ viewer,
+ request.community,
+ threadPermissions.CHANGE_ROLE,
+ );
+ if (!hasPermission) {
+ throw new ServerError('invalid_credentials');
+ }
+
+ const { community, roleID } = request;
+
+ const defaultRoleQuery = SQL`
+ SELECT default_role
+ FROM threads
+ WHERE id = ${community}
+ `;
+
+ const membersWithRoleQuery = SQL`
+ SELECT user
+ FROM memberships
+ WHERE thread = ${community}
+ AND role = ${roleID}
+ `;
+
+ const [[defaultRoleResult], [membersWithRoleResult]] = await Promise.all([
+ dbQuery(defaultRoleQuery),
+ dbQuery(membersWithRoleQuery),
+ ]);
+ const defaultRole = defaultRoleResult[0].default_role.toString();
+ const membersWithRole = membersWithRoleResult.map(result => result.user);
+
+ if (roleID === defaultRole) {
+ throw new ServerError('invalid_parameters');
+ }
+
+ if (membersWithRole.length > 0) {
+ await updateRole(viewer, {
+ threadID: community,
+ memberIDs: membersWithRole,
+ role: defaultRole,
+ });
+ }
+
+ const deleteFromRolesQuery = SQL`
+ DELETE FROM roles
+ WHERE id = ${roleID}
+ AND thread = ${community}
+ `;
+
+ await dbQuery(deleteFromRolesQuery);
+
+ const fetchServerThreadInfosResult = await fetchServerThreadInfos({
+ threadID: community,
+ });
+ const { threadInfos: serverThreadInfos } = fetchServerThreadInfosResult;
+ const serverThreadInfo = serverThreadInfos[community];
+
+ const time = Date.now();
+
+ const updateDatas = [];
+ for (const memberInfo of serverThreadInfo.members) {
+ updateDatas.push({
+ type: updateTypes.UPDATE_THREAD,
+ userID: memberInfo.id,
+ time,
+ threadID: community,
+ });
+ }
+
+ const { viewerUpdates } = await createUpdates(updateDatas, {
+ viewer,
+ updatesForCurrentSession: 'return',
+ });
+
+ const { threadInfos: rawThreadInfos } = rawThreadInfosFromServerThreadInfos(
+ viewer,
+ fetchServerThreadInfosResult,
+ );
+ const rawThreadInfo = rawThreadInfos[community];
+
+ return {
+ threadInfo: rawThreadInfo,
+ updatesResult: {
+ newUpdates: viewerUpdates,
+ },
+ };
+}
+
+export { deleteOrphanedRoles, deleteRole };
diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js
--- a/keyserver/src/endpoints.js
+++ b/keyserver/src/endpoints.js
@@ -60,6 +60,7 @@
threadJoinResponder,
toggleMessagePinResponder,
roleModificationResponder,
+ roleDeletionResponder,
} from './responders/thread-responders.js';
import {
userSubscriptionUpdateResponder,
@@ -144,6 +145,10 @@
responder: entryDeletionResponder,
requiredPolicies: baseLegalPolicies,
},
+ delete_community_role: {
+ responder: roleDeletionResponder,
+ requiredPolicies: baseLegalPolicies,
+ },
delete_thread: {
responder: threadDeletionResponder,
requiredPolicies: baseLegalPolicies,
diff --git a/keyserver/src/responders/thread-responders.js b/keyserver/src/responders/thread-responders.js
--- a/keyserver/src/responders/thread-responders.js
+++ b/keyserver/src/responders/thread-responders.js
@@ -28,6 +28,8 @@
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';
@@ -48,6 +50,7 @@
} 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';
@@ -384,6 +387,36 @@
);
}
+const roleDeletionRequestInputValidator = tShape<RoleDeletionRequest>({
+ community: tID,
+ roleID: tID,
+});
+
+export const roleDeletionResultValidator: TInterface<RoleDeletionResult> =
+ tShape<RoleDeletionResult>({
+ threadInfo: t.maybe(rawThreadInfoValidator),
+ updatesResult: tShape({
+ newUpdates: t.list(serverUpdateInfoValidator),
+ }),
+ });
+
+async function roleDeletionResponder(
+ viewer: Viewer,
+ input: mixed,
+): Promise<RoleDeletionResult> {
+ const request = await validateInput(
+ viewer,
+ roleDeletionRequestInputValidator,
+ input,
+ );
+ const response = await deleteRole(viewer, request);
+ return validateOutput(
+ viewer.platformDetails,
+ roleDeletionResultValidator,
+ response,
+ );
+}
+
export {
threadDeletionResponder,
roleUpdateResponder,
@@ -396,4 +429,5 @@
newThreadRequestInputValidator,
toggleMessagePinResponder,
roleModificationResponder,
+ roleDeletionResponder,
};
diff --git a/lib/actions/thread-actions.js b/lib/actions/thread-actions.js
--- a/lib/actions/thread-actions.js
+++ b/lib/actions/thread-actions.js
@@ -16,6 +16,8 @@
ToggleMessagePinResult,
RoleModificationRequest,
RoleModificationPayload,
+ RoleDeletionRequest,
+ RoleDeletionPayload,
} from '../types/thread-types.js';
import type { CallServerEndpoint } from '../utils/call-server-endpoint.js';
import { values } from '../utils/objects.js';
@@ -212,6 +214,23 @@
};
};
+const deleteCommunityRoleActionTypes = Object.freeze({
+ started: 'DELETE_COMMUNITY_ROLE_STARTED',
+ success: 'DELETE_COMMUNITY_ROLE_SUCCESS',
+ failed: 'DELETE_COMMUNITY_ROLE_FAILED',
+});
+const deleteCommunityRole =
+ (
+ callServerEndpoint: CallServerEndpoint,
+ ): ((request: RoleDeletionRequest) => Promise<RoleDeletionPayload>) =>
+ async request => {
+ const response = await callServerEndpoint('delete_community_role', request);
+ return {
+ threadInfo: response.threadInfo,
+ updatesResult: response.updatesResult,
+ };
+ };
+
export {
deleteThreadActionTypes,
deleteThread,
@@ -232,4 +251,6 @@
toggleMessagePin,
modifyCommunityRoleActionTypes,
modifyCommunityRole,
+ deleteCommunityRoleActionTypes,
+ deleteCommunityRole,
};
diff --git a/lib/reducers/thread-reducer.js b/lib/reducers/thread-reducer.js
--- a/lib/reducers/thread-reducer.js
+++ b/lib/reducers/thread-reducer.js
@@ -18,6 +18,7 @@
joinThreadActionTypes,
leaveThreadActionTypes,
modifyCommunityRoleActionTypes,
+ deleteCommunityRoleActionTypes,
} from '../actions/thread-actions.js';
import {
logOutActionTypes,
@@ -222,7 +223,8 @@
action.type === incrementalStateSyncActionType ||
action.type === processUpdatesActionType ||
action.type === newThreadActionTypes.success ||
- action.type === modifyCommunityRoleActionTypes.success
+ action.type === modifyCommunityRoleActionTypes.success ||
+ action.type === deleteCommunityRoleActionTypes.success
) {
const { newUpdates } = action.payload.updatesResult;
if (newUpdates.length === 0) {
diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js
--- a/lib/types/endpoints.js
+++ b/lib/types/endpoints.js
@@ -57,6 +57,7 @@
CREATE_TEXT_MESSAGE: 'create_text_message',
CREATE_THREAD: 'create_thread',
DELETE_ENTRY: 'delete_entry',
+ DELETE_COMMUNITY_ROLE: 'delete_community_role',
DELETE_THREAD: 'delete_thread',
DELETE_UPLOAD: 'delete_upload',
DISABLE_INVITE_LINK: 'disable_invite_link',
diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js
--- a/lib/types/redux-types.js
+++ b/lib/types/redux-types.js
@@ -109,6 +109,7 @@
ThreadJoinPayload,
ToggleMessagePinResult,
RoleModificationPayload,
+ RoleDeletionPayload,
} from './thread-types.js';
import type { ClientUpdatesResultWithUserInfos } from './update-types.js';
import type { CurrentUserInfo, UserStore } from './user-types.js';
@@ -1185,6 +1186,22 @@
+error: true,
+payload: Error,
+loadingInfo: LoadingInfo,
+ }
+ | {
+ +type: 'DELETE_COMMUNITY_ROLE_STARTED',
+ +loadingInfo?: LoadingInfo,
+ +payload?: void,
+ }
+ | {
+ +type: 'DELETE_COMMUNITY_ROLE_SUCCESS',
+ +payload: RoleDeletionPayload,
+ +loadingInfo: LoadingInfo,
+ }
+ | {
+ +type: 'DELETE_COMMUNITY_ROLE_FAILED',
+ +error: true,
+ +payload: Error,
+ +loadingInfo: LoadingInfo,
};
export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string);
diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js
--- a/lib/types/thread-types.js
+++ b/lib/types/thread-types.js
@@ -448,6 +448,25 @@
},
};
+export type RoleDeletionRequest = {
+ +community: string,
+ +roleID: string,
+};
+
+export type RoleDeletionResult = {
+ +threadInfo: RawThreadInfo,
+ +updatesResult: {
+ +newUpdates: $ReadOnlyArray<ServerUpdateInfo>,
+ },
+};
+
+export type RoleDeletionPayload = {
+ +threadInfo: RawThreadInfo,
+ +updatesResult: {
+ +newUpdates: $ReadOnlyArray<ClientUpdateInfo>,
+ },
+};
+
// 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;
diff --git a/native/roles/role-panel-entry.react.js b/native/roles/role-panel-entry.react.js
--- a/native/roles/role-panel-entry.react.js
+++ b/native/roles/role-panel-entry.react.js
@@ -9,6 +9,7 @@
import type { UserSurfacedPermission } from 'lib/types/thread-permission-types.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
+import { useDisplayDeleteRoleAlert } from './role-utils.react.js';
import type { RolesNavigationProp } from './roles-navigator.react.js';
import CommIcon from '../components/comm-icon.react.js';
import SWMansionIcon from '../components/swmansion-icon.react.js';
@@ -38,15 +39,33 @@
);
invariant(existingRoleID, 'Role ID must exist for an existing role');
+ const defaultRoleID = Object.keys(threadInfo.roles).find(
+ roleID => threadInfo.roles[roleID].isDefault,
+ );
+ invariant(defaultRoleID, 'Default role ID must exist');
+
+ const displayDeleteRoleAlert = useDisplayDeleteRoleAlert(
+ threadInfo,
+ existingRoleID,
+ defaultRoleID,
+ memberCount,
+ );
+
const options = React.useMemo(() => {
const availableOptions = ['Edit role'];
+ // Since the `Members` role is able to be renamed, we need to check if the
+ // default role ID is the same as the existing role ID.
+ if (defaultRoleID !== existingRoleID) {
+ availableOptions.push('Delete role');
+ }
+
if (Platform.OS === 'ios') {
availableOptions.push('Cancel');
}
return availableOptions;
- }, []);
+ }, [defaultRoleID, existingRoleID]);
const onOptionSelected = React.useCallback(
(index: ?number) => {
@@ -64,6 +83,8 @@
roleName,
rolePermissions,
});
+ } else if (selectedOption === 'Delete role') {
+ displayDeleteRoleAlert();
}
},
[
@@ -73,6 +94,7 @@
roleName,
rolePermissions,
threadInfo,
+ displayDeleteRoleAlert,
],
);
diff --git a/native/roles/role-utils.react.js b/native/roles/role-utils.react.js
new file mode 100644
--- /dev/null
+++ b/native/roles/role-utils.react.js
@@ -0,0 +1,70 @@
+// @flow
+
+import * as React from 'react';
+import { Alert } from 'react-native';
+
+import {
+ deleteCommunityRole,
+ deleteCommunityRoleActionTypes,
+} from 'lib/actions/thread-actions.js';
+import type { ThreadInfo } from 'lib/types/thread-types.js';
+import {
+ useDispatchActionPromise,
+ useServerCall,
+} from 'lib/utils/action-utils.js';
+
+function useDisplayDeleteRoleAlert(
+ threadInfo: ThreadInfo,
+ existingRoleID: string,
+ defaultRoleID: string,
+ memberCount: number,
+): () => void {
+ const defaultRoleName = threadInfo.roles[defaultRoleID].name;
+ const callDeleteCommunityRole = useServerCall(deleteCommunityRole);
+ const dispatchActionPromise = useDispatchActionPromise();
+
+ const onDeleteRole = React.useCallback(() => {
+ dispatchActionPromise(
+ deleteCommunityRoleActionTypes,
+ callDeleteCommunityRole({
+ community: threadInfo.id,
+ roleID: existingRoleID,
+ }),
+ );
+ }, [
+ callDeleteCommunityRole,
+ dispatchActionPromise,
+ existingRoleID,
+ threadInfo.id,
+ ]);
+
+ let message;
+ if (memberCount === 0) {
+ message = 'Are you sure you want to delete this role?';
+ } else {
+ const messageNoun = memberCount === 1 ? 'member' : 'members';
+ const messageVerb = memberCount === 1 ? 'is' : 'are';
+ message =
+ `There ${messageVerb} currently ${memberCount} ${messageNoun} with ` +
+ `this role. Deleting the role will automatically assign the members ` +
+ `affected to the ${defaultRoleName} role.`;
+ }
+
+ return React.useCallback(
+ () =>
+ Alert.alert('Delete role', message, [
+ {
+ text: 'Yes, delete role',
+ style: 'destructive',
+ onPress: onDeleteRole,
+ },
+ {
+ text: 'Cancel',
+ style: 'cancel',
+ },
+ ]),
+ [message, onDeleteRole],
+ );
+}
+
+export { useDisplayDeleteRoleAlert };

File Metadata

Mime Type
text/plain
Expires
Mon, Dec 8, 7:33 AM (39 m, 4 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5847454
Default Alt Text
D8574.1765179220.diff (14 KB)

Event Timeline