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
@@ -11,14 +11,19 @@
 import {
   type OutboundDMOperationSpecification,
   dmOperationSpecificationTypes,
+  getCreateThickRawThreadInfoInputFromThreadInfo,
 } from '../shared/dm-ops/dm-op-utils.js';
 import { useProcessAndSendDMOperation } from '../shared/dm-ops/process-dm-ops.js';
 import { permissionsAndAuthRelatedRequestTimeout } from '../shared/timeouts.js';
 import type {
   DMChangeThreadSettingsOperation,
   DMThreadSettingsChanges,
+  DMJoinThreadOperation,
 } from '../types/dm-ops.js';
-import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js';
+import type {
+  ThreadInfo,
+  RawThreadInfo,
+} from '../types/minimally-encoded-thread-permissions-types.js';
 import {
   thickThreadTypes,
   threadTypeIsThick,
@@ -346,10 +351,71 @@
     };
   };
 
+export type UseJoinThreadInput = $ReadOnly<
+  | {
+      +thick: false,
+      ...ClientThreadJoinRequest,
+    }
+  | {
+      +thick: true,
+      +rawThreadInfo: RawThreadInfo,
+    },
+>;
+
 function useJoinThread(): (
-  input: ClientThreadJoinRequest,
+  input: UseJoinThreadInput,
 ) => Promise<ThreadJoinPayload> {
-  return useKeyserverCall(joinThread);
+  const processAndSendDMOperation = useProcessAndSendDMOperation();
+  const viewerID = useSelector(
+    state => state.currentUserInfo && state.currentUserInfo.id,
+  );
+  const keyserverCall = useKeyserverCall(joinThread);
+  return React.useCallback(
+    async (input: UseJoinThreadInput) => {
+      if (!input.thick) {
+        const { thick, ...rest } = input;
+        return await keyserverCall({ ...rest });
+      }
+
+      const { rawThreadInfo } = input;
+
+      invariant(viewerID, 'viewerID should be set');
+      invariant(rawThreadInfo.thick, 'thread must be thick');
+
+      const existingThreadDetails =
+        getCreateThickRawThreadInfoInputFromThreadInfo(rawThreadInfo);
+
+      const op: DMJoinThreadOperation = {
+        type: 'join_thread',
+        joinerID: viewerID,
+        time: Date.now(),
+        messageID: uuid.v4(),
+        existingThreadDetails,
+      };
+
+      const opSpecification: OutboundDMOperationSpecification = {
+        type: dmOperationSpecificationTypes.OUTBOUND,
+        op,
+        recipients: {
+          type: 'all_thread_members',
+          threadID:
+            rawThreadInfo.type === thickThreadTypes.THICK_SIDEBAR &&
+            rawThreadInfo.parentThreadID
+              ? rawThreadInfo.parentThreadID
+              : rawThreadInfo.id,
+        },
+      };
+
+      await processAndSendDMOperation(opSpecification);
+      return ({
+        updatesResult: { newUpdates: [] },
+        rawMessageInfos: [],
+        truncationStatuses: {},
+        userInfos: [],
+      }: ThreadJoinPayload);
+    },
+    [keyserverCall, processAndSendDMOperation, viewerID],
+  );
 }
 
 export type LeaveThreadInput = {
diff --git a/lib/shared/community-utils.js b/lib/shared/community-utils.js
--- a/lib/shared/community-utils.js
+++ b/lib/shared/community-utils.js
@@ -210,6 +210,10 @@
 
   const dispatch = useDispatch();
   const dispatchActionPromise = useDispatchActionPromise();
+  const rawThreadInfo = useSelector(state =>
+    threadID ? state.threadStore.threadInfos[threadID] : null,
+  );
+
   const callJoinThread = useJoinThread();
 
   let keyserverID = keyserverOverride?.keyserverID;
@@ -339,6 +343,8 @@
       const communityThreadID = ongoingJoinData.communityID;
       const query = calendarQuery();
       const joinThreadPromise = callJoinThread({
+        thick: false,
+
         threadID: communityThreadID,
         calendarQuery: {
           startDate: query.startDate,
@@ -397,18 +403,29 @@
       }
 
       const query = calendarQuery();
-      const joinThreadPromise = callJoinThread({
-        threadID,
-        calendarQuery: {
-          startDate: query.startDate,
-          endDate: query.endDate,
-          filters: [
-            ...query.filters,
-            { type: 'threads', threadIDs: [threadID] },
-          ],
-        },
-        inviteLinkSecret: inviteSecret,
-      });
+      let joinThreadInput;
+      if (rawThreadInfo && rawThreadInfo.thick) {
+        joinThreadInput = {
+          thick: true,
+          rawThreadInfo,
+        };
+      } else {
+        joinThreadInput = {
+          thick: false,
+
+          threadID,
+          calendarQuery: {
+            startDate: query.startDate,
+            endDate: query.endDate,
+            filters: [
+              ...query.filters,
+              { type: 'threads', threadIDs: [threadID] },
+            ],
+          },
+        };
+      }
+
+      const joinThreadPromise = callJoinThread(joinThreadInput);
       void dispatchActionPromise(joinThreadActionTypes, joinThreadPromise);
 
       try {
@@ -429,6 +446,7 @@
       }
     })();
   }, [
+    rawThreadInfo,
     calendarQuery,
     callJoinThread,
     dispatchActionPromise,
diff --git a/lib/shared/dm-ops/dm-op-utils.js b/lib/shared/dm-ops/dm-op-utils.js
--- a/lib/shared/dm-ops/dm-op-utils.js
+++ b/lib/shared/dm-ops/dm-op-utils.js
@@ -239,4 +239,8 @@
   );
 }
 
-export { createMessagesToPeersFromDMOp, useAddDMThreadMembers };
+export {
+  createMessagesToPeersFromDMOp,
+  useAddDMThreadMembers,
+  getCreateThickRawThreadInfoInputFromThreadInfo,
+};
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
@@ -414,7 +414,7 @@
   +rawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
   +truncationStatuses: MessageTruncationStatuses,
   +userInfos: $ReadOnlyArray<UserInfo>,
-  +keyserverID: string,
+  +keyserverID?: string,
 };
 
 export type ThreadFetchMediaResult = {
diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js
--- a/native/chat/chat-input-bar.react.js
+++ b/native/chat/chat-input-bar.react.js
@@ -31,6 +31,7 @@
   newThreadActionTypes,
   useJoinThread,
 } from 'lib/actions/thread-actions.js';
+import type { UseJoinThreadInput } from 'lib/actions/thread-actions.js';
 import {
   useChatMentionContext,
   useThreadChatMentionCandidates,
@@ -73,12 +74,12 @@
 import type {
   RelativeMemberInfo,
   ThreadInfo,
+  RawThreadInfo,
 } from 'lib/types/minimally-encoded-thread-permissions-types.js';
 import type { Dispatch } from 'lib/types/redux-types.js';
 import { threadPermissions } from 'lib/types/thread-permission-types.js';
 import type {
   ChatMentionCandidates,
-  ClientThreadJoinRequest,
   ThreadJoinPayload,
 } from 'lib/types/thread-types.js';
 import {
@@ -276,6 +277,7 @@
 type Props = {
   ...BaseProps,
   +viewerID: ?string,
+  +rawThreadInfo: RawThreadInfo,
   +draft: string,
   +joinThreadLoadingStatus: LoadingStatus,
   +threadCreationInProgress: boolean,
@@ -288,7 +290,7 @@
   +keyboardState: ?KeyboardState,
   +dispatch: Dispatch,
   +dispatchActionPromise: DispatchActionPromise,
-  +joinThread: (request: ClientThreadJoinRequest) => Promise<ThreadJoinPayload>,
+  +joinThread: (input: UseJoinThreadInput) => Promise<ThreadJoinPayload>,
   +inputState: ?InputState,
   +userMentionsCandidates: $ReadOnlyArray<RelativeMemberInfo>,
   +chatMentionSearchIndex: ?SentencePrefixSearchIndex,
@@ -1143,18 +1145,29 @@
   };
 
   async joinAction(): Promise<ThreadJoinPayload> {
-    const query = this.props.calendarQuery();
-    return await this.props.joinThread({
-      threadID: this.props.threadInfo.id,
-      calendarQuery: {
-        startDate: query.startDate,
-        endDate: query.endDate,
-        filters: [
-          ...query.filters,
-          { type: 'threads', threadIDs: [this.props.threadInfo.id] },
-        ],
-      },
-    });
+    let joinThreadInput;
+    if (this.props.rawThreadInfo.thick) {
+      joinThreadInput = {
+        thick: true,
+        rawThreadInfo: this.props.rawThreadInfo,
+      };
+    } else {
+      const query = this.props.calendarQuery();
+      joinThreadInput = {
+        thick: false,
+        threadID: this.props.threadInfo.id,
+        calendarQuery: {
+          startDate: query.startDate,
+          endDate: query.endDate,
+          filters: [
+            ...query.filters,
+            { type: 'threads', threadIDs: [this.props.threadInfo.id] },
+          ],
+        },
+      };
+    }
+
+    return await this.props.joinThread(joinThreadInput);
   }
 
   expandButtons = () => {
@@ -1243,6 +1256,9 @@
 
   const dispatch = useDispatch();
   const dispatchActionPromise = useDispatchActionPromise();
+  const rawThreadInfo = useSelector(
+    state => state.threadStore.threadInfos[props.threadInfo.id],
+  );
   const callJoinThread = useJoinThread();
 
   const { getChatMentionSearchIndex } = useChatMentionContext();
@@ -1340,6 +1356,7 @@
     <ChatInputBar
       {...props}
       viewerID={viewerID}
+      rawThreadInfo={rawThreadInfo}
       draft={draft}
       joinThreadLoadingStatus={joinThreadLoadingStatus}
       threadCreationInProgress={threadCreationInProgress}
diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js
--- a/web/chat/chat-input-bar.react.js
+++ b/web/chat/chat-input-bar.react.js
@@ -9,6 +9,7 @@
   newThreadActionTypes,
   useJoinThread,
 } from 'lib/actions/thread-actions.js';
+import type { UseJoinThreadInput } from 'lib/actions/thread-actions.js';
 import SWMansionIcon from 'lib/components/swmansion-icon.react.js';
 import {
   useChatMentionContext,
@@ -35,12 +36,12 @@
 import type { CalendarQuery } from 'lib/types/entry-types.js';
 import type { LoadingStatus } from 'lib/types/loading-types.js';
 import { messageTypes } from 'lib/types/message-types-enum.js';
-import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
+import type {
+  ThreadInfo,
+  RawThreadInfo,
+} from 'lib/types/minimally-encoded-thread-permissions-types.js';
 import { threadPermissions } from 'lib/types/thread-permission-types.js';
-import {
-  type ClientThreadJoinRequest,
-  type ThreadJoinPayload,
-} from 'lib/types/thread-types.js';
+import { type ThreadJoinPayload } from 'lib/types/thread-types.js';
 import {
   type DispatchActionPromise,
   useDispatchActionPromise,
@@ -71,12 +72,13 @@
 type Props = {
   ...BaseProps,
   +viewerID: ?string,
+  +rawThreadInfo: RawThreadInfo,
   +joinThreadLoadingStatus: LoadingStatus,
   +threadCreationInProgress: boolean,
   +calendarQuery: () => CalendarQuery,
   +isThreadActive: boolean,
   +dispatchActionPromise: DispatchActionPromise,
-  +joinThread: (request: ClientThreadJoinRequest) => Promise<ThreadJoinPayload>,
+  +joinThread: (input: UseJoinThreadInput) => Promise<ThreadJoinPayload>,
   +typeaheadMatchedStrings: ?TypeaheadMatchedStrings,
   +suggestions: $ReadOnlyArray<MentionTypeaheadSuggestionItem>,
   +parentThreadInfo: ?ThreadInfo,
@@ -534,18 +536,29 @@
   };
 
   async joinAction(): Promise<ThreadJoinPayload> {
-    const query = this.props.calendarQuery();
-    return await this.props.joinThread({
-      threadID: this.props.threadInfo.id,
-      calendarQuery: {
-        startDate: query.startDate,
-        endDate: query.endDate,
-        filters: [
-          ...query.filters,
-          { type: 'threads', threadIDs: [this.props.threadInfo.id] },
-        ],
-      },
-    });
+    let joinThreadInput;
+    if (this.props.rawThreadInfo.thick) {
+      joinThreadInput = {
+        thick: true,
+        rawThreadInfo: this.props.rawThreadInfo,
+      };
+    } else {
+      const query = this.props.calendarQuery();
+      joinThreadInput = {
+        thick: false,
+        threadID: this.props.threadInfo.id,
+        calendarQuery: {
+          startDate: query.startDate,
+          endDate: query.endDate,
+          filters: [
+            ...query.filters,
+            { type: 'threads', threadIDs: [this.props.threadInfo.id] },
+          ],
+        },
+      };
+    }
+
+    return await this.props.joinThread(joinThreadInput);
   }
 }
 
@@ -573,6 +586,9 @@
     const threadCreationInProgress = createThreadLoadingStatus === 'loading';
     const calendarQuery = useSelector(nonThreadCalendarQuery);
     const dispatchActionPromise = useDispatchActionPromise();
+    const rawThreadInfo = useSelector(
+      state => state.threadStore.threadInfos[props.threadInfo.id],
+    );
     const callJoinThread = useJoinThread();
     const { getChatMentionSearchIndex } = useChatMentionContext();
     const chatMentionSearchIndex = getChatMentionSearchIndex(props.threadInfo);
@@ -672,6 +688,7 @@
       <ChatInputBar
         {...props}
         viewerID={viewerID}
+        rawThreadInfo={rawThreadInfo}
         joinThreadLoadingStatus={joinThreadLoadingStatus}
         threadCreationInProgress={threadCreationInProgress}
         calendarQuery={calendarQuery}