diff --git a/lib/shared/dm-ops/add-members-spec.js b/lib/shared/dm-ops/add-members-spec.js
--- a/lib/shared/dm-ops/add-members-spec.js
+++ b/lib/shared/dm-ops/add-members-spec.js
@@ -28,113 +28,132 @@
 import { values } from '../../utils/objects.js';
 import { roleIsDefaultRole, userIsMember } from '../thread-utils.js';
 
+function createAddNewMembersResults(
+  dmOperation: DMAddMembersOperation,
+  viewerID: string,
+  utilities: ProcessDMOperationUtilities,
+): {
+  +rawMessageInfos: Array<RawMessageInfo>,
+  +updateInfos: Array<ClientUpdateInfo>,
+  +threadInfo: ?ThickRawThreadInfo,
+} {
+  const { editorID, time, messageID, addedUserIDs, existingThreadDetails } =
+    dmOperation;
+  const addMembersMessage = {
+    type: messageTypes.ADD_MEMBERS,
+    id: messageID,
+    threadID: existingThreadDetails.threadID,
+    creatorID: editorID,
+    time,
+    addedUserIDs: [...addedUserIDs],
+  };
+
+  const viewerIsAdded = addedUserIDs.includes(viewerID);
+  const updateInfos: Array<ClientUpdateInfo> = [];
+  const rawMessageInfos: Array<RawMessageInfo> = [];
+  let resultThreadInfo: ?ThickRawThreadInfo;
+  if (viewerIsAdded) {
+    const newThread = createThickRawThreadInfo(
+      {
+        ...existingThreadDetails,
+        allMemberIDs: [...existingThreadDetails.allMemberIDs, ...addedUserIDs],
+      },
+      viewerID,
+    );
+    resultThreadInfo = newThread;
+    updateInfos.push(
+      {
+        type: updateTypes.JOIN_THREAD,
+        id: uuid.v4(),
+        time,
+        threadInfo: newThread,
+        rawMessageInfos: [addMembersMessage],
+        truncationStatus: messageTruncationStatus.EXHAUSTIVE,
+        rawEntryInfos: [],
+      },
+      {
+        type: updateTypes.UPDATE_THREAD_READ_STATUS,
+        id: uuid.v4(),
+        time,
+        threadID: existingThreadDetails.threadID,
+        unread: true,
+      },
+    );
+  } else {
+    const currentThreadInfoOptional =
+      utilities.threadInfos[existingThreadDetails.threadID];
+    if (!currentThreadInfoOptional || !currentThreadInfoOptional.thick) {
+      // We can't perform this operation now. It should be queued for later.
+      return {
+        rawMessageInfos: [],
+        updateInfos: [],
+        threadInfo: null,
+      };
+    }
+    const currentThreadInfo: ThickRawThreadInfo = currentThreadInfoOptional;
+    const defaultRoleID = values(currentThreadInfo.roles).find(role =>
+      roleIsDefaultRole(role),
+    )?.id;
+    invariant(defaultRoleID, 'Default role ID must exist');
+    const { membershipPermissions } = createRoleAndPermissionForThickThreads(
+      currentThreadInfo.type,
+      currentThreadInfo.id,
+      defaultRoleID,
+    );
+    const newMembers = addedUserIDs
+      .filter(userID => !userIsMember(currentThreadInfo, userID))
+      .map(userID =>
+        minimallyEncodeMemberInfo<ThickMemberInfo>({
+          id: userID,
+          role: defaultRoleID,
+          permissions: membershipPermissions,
+          isSender: editorID === viewerID,
+          subscription: joinThreadSubscription,
+        }),
+      );
+
+    const newThreadInfo = {
+      ...currentThreadInfo,
+      members: [...currentThreadInfo.members, ...newMembers],
+    };
+    resultThreadInfo = newThreadInfo;
+    updateInfos.push(
+      {
+        type: updateTypes.UPDATE_THREAD,
+        id: uuid.v4(),
+        time,
+        threadInfo: newThreadInfo,
+      },
+      {
+        type: updateTypes.UPDATE_THREAD_READ_STATUS,
+        id: uuid.v4(),
+        time,
+        threadID: existingThreadDetails.threadID,
+        unread: true,
+      },
+    );
+    rawMessageInfos.push(addMembersMessage);
+  }
+  return {
+    rawMessageInfos,
+    updateInfos,
+    threadInfo: resultThreadInfo,
+  };
+}
+
 const addMembersSpec: DMOperationSpec<DMAddMembersOperation> = Object.freeze({
   processDMOperation: async (
     dmOperation: DMAddMembersOperation,
     viewerID: string,
     utilities: ProcessDMOperationUtilities,
   ) => {
-    const { editorID, time, messageID, addedUserIDs, existingThreadDetails } =
-      dmOperation;
-    const addMembersMessage = {
-      type: messageTypes.ADD_MEMBERS,
-      id: messageID,
-      threadID: existingThreadDetails.threadID,
-      creatorID: editorID,
-      time,
-      addedUserIDs: [...addedUserIDs],
-    };
-
-    const viewerIsAdded = addedUserIDs.includes(viewerID);
-    const updateInfos: Array<ClientUpdateInfo> = [];
-    const rawMessageInfos: Array<RawMessageInfo> = [];
-    if (viewerIsAdded) {
-      const newThread = createThickRawThreadInfo(
-        {
-          ...existingThreadDetails,
-          allMemberIDs: [
-            ...existingThreadDetails.allMemberIDs,
-            ...addedUserIDs,
-          ],
-        },
-        viewerID,
-      );
-      updateInfos.push(
-        {
-          type: updateTypes.JOIN_THREAD,
-          id: uuid.v4(),
-          time,
-          threadInfo: newThread,
-          rawMessageInfos: [addMembersMessage],
-          truncationStatus: messageTruncationStatus.EXHAUSTIVE,
-          rawEntryInfos: [],
-        },
-        {
-          type: updateTypes.UPDATE_THREAD_READ_STATUS,
-          id: uuid.v4(),
-          time,
-          threadID: existingThreadDetails.threadID,
-          unread: true,
-        },
-      );
-    } else {
-      const currentThreadInfoOptional =
-        utilities.threadInfos[existingThreadDetails.threadID];
-      if (!currentThreadInfoOptional || !currentThreadInfoOptional.thick) {
-        // We can't perform this operation now. It should be queued for later.
-        return {
-          rawMessageInfos: [],
-          updateInfos: [],
-        };
-      }
-      const currentThreadInfo: ThickRawThreadInfo = currentThreadInfoOptional;
-      const defaultRoleID = values(currentThreadInfo.roles).find(role =>
-        roleIsDefaultRole(role),
-      )?.id;
-      invariant(defaultRoleID, 'Default role ID must exist');
-      const { membershipPermissions } = createRoleAndPermissionForThickThreads(
-        currentThreadInfo.type,
-        currentThreadInfo.id,
-        defaultRoleID,
-      );
-      const newMembers = addedUserIDs
-        .filter(userID => !userIsMember(currentThreadInfo, userID))
-        .map(userID =>
-          minimallyEncodeMemberInfo<ThickMemberInfo>({
-            id: userID,
-            role: defaultRoleID,
-            permissions: membershipPermissions,
-            isSender: editorID === viewerID,
-            subscription: joinThreadSubscription,
-          }),
-        );
-
-      const newThreadInfo = {
-        ...currentThreadInfo,
-        members: [...currentThreadInfo.members, ...newMembers],
-      };
-      updateInfos.push(
-        {
-          type: updateTypes.UPDATE_THREAD,
-          id: uuid.v4(),
-          time,
-          threadInfo: newThreadInfo,
-        },
-        {
-          type: updateTypes.UPDATE_THREAD_READ_STATUS,
-          id: uuid.v4(),
-          time,
-          threadID: existingThreadDetails.threadID,
-          unread: true,
-        },
-      );
-      rawMessageInfos.push(addMembersMessage);
-    }
-    return {
-      rawMessageInfos,
-      updateInfos,
-    };
+    const { rawMessageInfos, updateInfos } = createAddNewMembersResults(
+      dmOperation,
+      viewerID,
+      utilities,
+    );
+    return { rawMessageInfos, updateInfos };
   },
 });
 
-export { addMembersSpec };
+export { addMembersSpec, createAddNewMembersResults };
diff --git a/lib/shared/dm-ops/change-thread-settings-spec.js b/lib/shared/dm-ops/change-thread-settings-spec.js
new file mode 100644
--- /dev/null
+++ b/lib/shared/dm-ops/change-thread-settings-spec.js
@@ -0,0 +1,155 @@
+// @flow
+
+import uuid from 'uuid';
+
+import { createAddNewMembersResults } from './add-members-spec.js';
+import type {
+  DMOperationSpec,
+  ProcessDMOperationUtilities,
+} from './dm-op-spec.js';
+import type { DMChangeThreadSettingsOperation } from '../../types/dm-ops.js';
+import { messageTypes } from '../../types/message-types-enum.js';
+import type { RawMessageInfo } from '../../types/message-types.js';
+import type { RawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js';
+import type { LegacyRawThreadInfo } from '../../types/thread-types.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+import type { ClientUpdateInfo } from '../../types/update-types.js';
+import { values } from '../../utils/objects.js';
+
+const changeThreadSettingsSpec: DMOperationSpec<DMChangeThreadSettingsOperation> =
+  Object.freeze({
+    processDMOperation: async (
+      dmOperation: DMChangeThreadSettingsOperation,
+      viewerID: string,
+      utilities: ProcessDMOperationUtilities,
+    ) => {
+      const {
+        editorID,
+        time,
+        changes,
+        messageIDsPrefix,
+        existingThreadDetails,
+      } = dmOperation;
+      const { name, description, color, avatar } = changes;
+      const threadID = existingThreadDetails.threadID;
+
+      const newMemberIDs =
+        changes.newMemberIDs && changes.newMemberIDs.length > 0
+          ? [...new Set(changes.newMemberIDs)]
+          : null;
+
+      let threadInfoToUpdate: ?(RawThreadInfo | LegacyRawThreadInfo) =
+        utilities.threadInfos[threadID];
+      if (!threadInfoToUpdate && !newMemberIDs?.includes(viewerID)) {
+        // We can't perform this operation now. It should be queued for later.
+        return {
+          rawMessageInfos: [],
+          updateInfos: [],
+        };
+      }
+
+      const updateInfos: Array<ClientUpdateInfo> = [];
+      const rawMessageInfos: Array<RawMessageInfo> = [];
+
+      if (newMemberIDs) {
+        const addMembersResult = createAddNewMembersResults(
+          {
+            type: 'add_members',
+            editorID,
+            time,
+            messageID: `${messageIDsPrefix}/add_members`,
+            addedUserIDs: newMemberIDs,
+            existingThreadDetails,
+          },
+          viewerID,
+          utilities,
+        );
+        if (addMembersResult.threadInfo) {
+          threadInfoToUpdate = addMembersResult.threadInfo;
+        }
+        updateInfos.push(...addMembersResult.updateInfos);
+        rawMessageInfos.push(...addMembersResult.rawMessageInfos);
+      }
+
+      if (!threadInfoToUpdate || !threadInfoToUpdate.thick) {
+        // We can't perform this operation now. It should be queued for later.
+        return {
+          rawMessageInfos: [],
+          updateInfos: [],
+        };
+      }
+
+      const changedFields: { [string]: string | number } = {};
+
+      if (name !== undefined && name !== null) {
+        changedFields.name = name;
+        threadInfoToUpdate = {
+          ...threadInfoToUpdate,
+          name,
+        };
+      }
+
+      if (description !== undefined && description !== null) {
+        changedFields.description = description;
+        threadInfoToUpdate = {
+          ...threadInfoToUpdate,
+          description,
+        };
+      }
+
+      if (color) {
+        changedFields.color = color;
+        threadInfoToUpdate = {
+          ...threadInfoToUpdate,
+          color,
+        };
+      }
+
+      if (avatar) {
+        changedFields.avatar = JSON.stringify(avatar);
+        threadInfoToUpdate = {
+          ...threadInfoToUpdate,
+          avatar,
+        };
+      }
+
+      for (const fieldName in changedFields) {
+        const newValue = changedFields[fieldName];
+        rawMessageInfos.push({
+          type: messageTypes.CHANGE_SETTINGS,
+          threadID,
+          creatorID: editorID,
+          time,
+          field: fieldName,
+          value: newValue,
+          id: `${messageIDsPrefix}/${fieldName}`,
+        });
+      }
+
+      if (values(changedFields).length > 0) {
+        updateInfos.push({
+          type: updateTypes.UPDATE_THREAD,
+          id: uuid.v4(),
+          time,
+          threadInfo: threadInfoToUpdate,
+        });
+      }
+
+      if (rawMessageInfos.length > 0) {
+        updateInfos.push({
+          type: updateTypes.UPDATE_THREAD_READ_STATUS,
+          id: uuid.v4(),
+          time,
+          threadID,
+          unread: true,
+        });
+      }
+
+      return {
+        rawMessageInfos,
+        updateInfos,
+      };
+    },
+  });
+
+export { changeThreadSettingsSpec };
diff --git a/lib/shared/dm-ops/dm-op-specs.js b/lib/shared/dm-ops/dm-op-specs.js
--- a/lib/shared/dm-ops/dm-op-specs.js
+++ b/lib/shared/dm-ops/dm-op-specs.js
@@ -1,6 +1,7 @@
 // @flow
 
 import { addMembersSpec } from './add-members-spec.js';
+import { changeThreadSettingsSpec } from './change-thread-settings-spec.js';
 import { createSidebarSpec } from './create-sidebar-spec.js';
 import { createThreadSpec } from './create-thread-spec.js';
 import type { DMOperationSpec } from './dm-op-spec.js';
@@ -24,4 +25,5 @@
   [dmOperationTypes.JOIN_THREAD]: joinThreadSpec,
   [dmOperationTypes.LEAVE_THREAD]: leaveThreadSpec,
   [dmOperationTypes.REMOVE_MEMBERS]: removeMembersSpec,
+  [dmOperationTypes.CHANGE_THREAD_SETTINGS]: changeThreadSettingsSpec,
 });
diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js
--- a/lib/types/dm-ops.js
+++ b/lib/types/dm-ops.js
@@ -13,7 +13,7 @@
 } from './thread-types-enum.js';
 import type { ClientUpdateInfo } from './update-types.js';
 import { values } from '../utils/objects.js';
-import { tShape, tString, tUserID } from '../utils/validation-utils.js';
+import { tColor, tShape, tString, tUserID } from '../utils/validation-utils.js';
 
 export const dmOperationTypes = Object.freeze({
   CREATE_THREAD: 'create_thread',
@@ -25,6 +25,7 @@
   JOIN_THREAD: 'join_thread',
   LEAVE_THREAD: 'leave_thread',
   REMOVE_MEMBERS: 'remove_members',
+  CHANGE_THREAD_SETTINGS: 'change_thread_settings',
 });
 export type DMOperationType = $Values<typeof dmOperationTypes>;
 
@@ -242,6 +243,36 @@
     removedUserIDs: t.list(tUserID),
   });
 
+export type DMChangeThreadSettingsOperation = {
+  +type: 'change_thread_settings',
+  +editorID: string,
+  +time: number,
+  +changes: {
+    +name?: string,
+    +description?: string,
+    +color?: string,
+    +newMemberIDs?: $ReadOnlyArray<string>,
+    +avatar?: ClientAvatar,
+  },
+  +messageIDsPrefix: string,
+  +existingThreadDetails: CreateThickRawThreadInfoInput,
+};
+export const dmChangeThreadSettingsOperationValidator: TInterface<DMChangeThreadSettingsOperation> =
+  tShape<DMChangeThreadSettingsOperation>({
+    type: tString(dmOperationTypes.CHANGE_THREAD_SETTINGS),
+    editorID: tUserID,
+    time: t.Number,
+    changes: tShape({
+      name: t.maybe(t.String),
+      description: t.maybe(t.String),
+      color: t.maybe(tColor),
+      newMemberIDs: t.maybe(t.list(tUserID)),
+      avatar: t.maybe(clientAvatarValidator),
+    }),
+    messageIDsPrefix: t.String,
+    existingThreadDetails: createThickRawThreadInfoInputValidator,
+  });
+
 export type DMOperation =
   | DMCreateThreadOperation
   | DMCreateSidebarOperation
@@ -251,7 +282,8 @@
   | DMAddMembersOperation
   | DMJoinThreadOperation
   | DMLeaveThreadOperation
-  | DMRemoveMembersOperation;
+  | DMRemoveMembersOperation
+  | DMChangeThreadSettingsOperation;
 export const dmOperationValidator: TUnion<DMOperation> = t.union([
   dmCreateThreadOperationValidator,
   dmCreateSidebarOperationValidator,
@@ -262,6 +294,7 @@
   dmJoinThreadOperationValidator,
   dmLeaveThreadOperationValidator,
   dmRemoveMembersOperationValidator,
+  dmChangeThreadSettingsOperationValidator,
 ]);
 
 export type DMOperationResult = {