diff --git a/web/modals/chat/toggle-pin-modal.css b/web/modals/chat/toggle-pin-modal.css
--- a/web/modals/chat/toggle-pin-modal.css
+++ b/web/modals/chat/toggle-pin-modal.css
@@ -0,0 +1,54 @@
+.confirmationText {
+    color: var(--pin-message-information-text-color);
+    padding: 16px 32px 0 32px;
+    font-size: small;
+}
+
+.messageContainer {
+    overflow-y: scroll;
+    border: 1px solid var(--pin-message-modal-border-color);
+    border-radius: 7px;
+    max-height: 400px;
+    margin: 16px 32px 16px 32px;
+}
+
+.messageDate {
+    color: var(--chat-timestamp-color);
+    font-size: var(--xs-font-12);
+    padding: 6px 0;
+    line-height: var(--line-height-text);
+    text-align: center;
+}
+
+.creator {
+    font-size: small;
+    color: #777777;
+    font-size: 14px;
+    padding: 4px 24px;
+    text-align: left;
+}
+
+.messageContent {
+    padding: 0 0 6px 0;
+}
+
+.buttonContainer {
+    width: 87%;
+    display: flex;
+    flex-direction: column;
+    align-self: center;
+    align-items: stretch;
+    margin-bottom: 16px;
+}
+
+.cancelButton {
+    color: white;
+    display: flex;
+    justify-content: center;
+    margin-top: 16px;
+}
+
+.cancelButton:hover {
+    cursor: pointer;
+    text-decoration: underline;
+}
diff --git a/web/modals/chat/toggle-pin-modal.react.js b/web/modals/chat/toggle-pin-modal.react.js
--- a/web/modals/chat/toggle-pin-modal.react.js
+++ b/web/modals/chat/toggle-pin-modal.react.js
@@ -1,18 +1,157 @@
 // @flow
 
+import invariant from 'invariant';
 import * as React from 'react';
 
+import {
+  toggleMessagePin,
+  toggleMessagePinActionTypes,
+} from 'lib/actions/thread-actions.js';
+import { useModalContext } from 'lib/components/modal-provider.react.js';
+import { useStringForUser } from 'lib/hooks/ens-cache.js';
 import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js';
 import type { ThreadInfo } from 'lib/types/thread-types.js';
+import type { RelativeUserInfo } from 'lib/types/user-types.js';
+import {
+  useServerCall,
+  useDispatchActionPromise,
+} from 'lib/utils/action-utils.js';
+import { longAbsoluteDate } from 'lib/utils/date-utils.js';
+
+import css from './toggle-pin-modal.css';
+import { MessageListContext } from '../../chat/message-list-types.js';
+import Message from '../../chat/message.react.js';
+import Button, { buttonThemes } from '../../components/button.react.js';
+import { useTextMessageRulesFunc } from '../../markdown/rules.react.js';
+import Modal from '../modal.react.js';
 
 type TogglePinModalProps = {
   +item: ChatMessageInfoItem,
   +threadInfo: ThreadInfo,
 };
 
-// eslint-disable-next-line no-unused-vars
 function TogglePinModal(props: TogglePinModalProps): React.Node {
-  return <></>;
+  const { item, threadInfo } = props;
+  const { messageInfo, isPinned } = item;
+  const { creator } = messageInfo;
+  const { popModal } = useModalContext();
+
+  const callToggleMessagePin = useServerCall(toggleMessagePin);
+  const dispatchActionPromise = useDispatchActionPromise();
+
+  const modalName = isPinned ? 'Remove Pinned Message' : 'Pin Message';
+  const action = isPinned ? 'unpin' : 'pin';
+  const confirmationText = isPinned
+    ? `Are you sure you want to remove this pinned message?`
+    : `You may pin this message to the channel you are currently viewing. 
+       To unpin a message, select the pinned messages icon in the channel. `;
+  const buttonText = isPinned ? 'Remove Pinned Message' : 'Pin Message';
+  const buttonColor = isPinned ? buttonThemes.danger : buttonThemes.standard;
+
+  const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadInfo);
+  const messageListContext = React.useMemo(() => {
+    if (!getTextMessageMarkdownRules) {
+      return undefined;
+    }
+    return { getTextMessageMarkdownRules };
+  }, [getTextMessageMarkdownRules]);
+
+  const creatorWithViewerFalse: RelativeUserInfo = React.useMemo(() => {
+    return {
+      ...creator,
+      isViewer: false,
+    };
+  }, [creator]);
+  const shouldShowUsername = !item.startsConversation && !item.startsCluster;
+  const username = useStringForUser(
+    shouldShowUsername ? creatorWithViewerFalse : null,
+  );
+
+  const shouldShowDate = !item.startsConversation;
+  const messageDate = shouldShowDate ? (
+    <div className={css.messageDate}>
+      {longAbsoluteDate(item.messageInfo.time)}
+    </div>
+  ) : null;
+
+  // ChatMessageInfoItem is a union type, and both types have different
+  // properties, so creating a new item by just using the ... operator
+  // results in Flow errors. Here, we check if the item is composable
+  // (the only item types that can be pinned), and if so we modify the
+  // creator to be 'creatorWithViewerFalse', which is the same creator except
+  // with the isViewer property to be false. This is so the message is
+  // rendered left-aligned and the creator's name is shown (per the designs).
+  // If the item is not composable, we just return the item as is. We
+  // also set the threadCreatedFromMessage property to undefined and
+  // the reactions property to an empty array, to remove the inline
+  // engagement bar from the message within the modal.
+  const modifiedItem = React.useMemo(() => {
+    if (item.messageInfoType === 'composable') {
+      return {
+        ...item,
+        threadCreatedFromMessage: undefined,
+        reactions: {},
+        messageInfo: {
+          ...item.messageInfo,
+          creator: creatorWithViewerFalse,
+        },
+      };
+    } else {
+      return item;
+    }
+  }, [item, creatorWithViewerFalse]);
+
+  const onClick = React.useCallback(() => {
+    const toggleMessagePinPromise = (async () => {
+      invariant(messageInfo.id, 'messageInfo.id should be defined');
+      const result = await callToggleMessagePin({
+        messageID: messageInfo.id,
+        action,
+      });
+      return {
+        newMessageInfos: result.newMessageInfos,
+        threadID: result.threadID,
+      };
+    })();
+
+    dispatchActionPromise(toggleMessagePinActionTypes, toggleMessagePinPromise);
+    popModal();
+  }, [
+    action,
+    callToggleMessagePin,
+    dispatchActionPromise,
+    messageInfo.id,
+    popModal,
+  ]);
+
+  return (
+    <Modal name={modalName} onClose={popModal} size="large">
+      <div className={css.confirmationText}>{confirmationText}</div>
+      <div className={css.messageContainer}>
+        <div>
+          {messageDate}
+          <div className={css.creator}>{username}</div>
+          <div className={css.messageContent}>
+            <MessageListContext.Provider value={messageListContext}>
+              <Message
+                item={modifiedItem}
+                threadInfo={threadInfo}
+                key={messageInfo.id}
+              />
+            </MessageListContext.Provider>
+          </div>
+        </div>
+      </div>
+      <div className={css.buttonContainer}>
+        <Button variant="filled" buttonColor={buttonColor} onClick={onClick}>
+          {buttonText}
+        </Button>
+        <div className={css.cancelButton} onClick={popModal}>
+          Cancel
+        </div>
+      </div>
+    </Modal>
+  );
 }
 
 export default TogglePinModal;
diff --git a/web/theme.css b/web/theme.css
--- a/web/theme.css
+++ b/web/theme.css
@@ -212,4 +212,6 @@
   --topbar-button-fg: var(--shades-white-60);
   --message-label-color: var(--shades-black-60);
   --topbar-lines: rgba(255, 255, 255, 0.08);
+  --pin-message-information-text-color: var(--shades-white-60);
+  --pin-message-modal-border-color: var(--shades-black-80);
 }
diff --git a/web/utils/tooltip-utils.js b/web/utils/tooltip-utils.js
--- a/web/utils/tooltip-utils.js
+++ b/web/utils/tooltip-utils.js
@@ -495,6 +495,8 @@
     isComposableMessageType(messageInfo.type) &&
     threadHasPermission(threadInfo, threadPermissions.MANAGE_PINS);
 
+  const inputState = React.useContext(InputStateContext);
+
   return React.useMemo(() => {
     if (!canTogglePin) {
       return null;
@@ -505,7 +507,11 @@
     const buttonContent = <SWMansionIcon icon={iconName} size={18} />;
 
     const onClickTogglePin = () => {
-      pushModal(<TogglePinModal item={item} threadInfo={threadInfo} />);
+      pushModal(
+        <InputStateContext.Provider value={inputState}>
+          <TogglePinModal item={item} threadInfo={threadInfo} />
+        </InputStateContext.Provider>,
+      );
     };
 
     return {
@@ -513,7 +519,7 @@
       onClick: onClickTogglePin,
       label: isPinned ? 'Unpin' : 'Pin',
     };
-  }, [canTogglePin, isPinned, pushModal, item, threadInfo]);
+  }, [canTogglePin, inputState, isPinned, pushModal, item, threadInfo]);
 }
 
 function useMessageTooltipActions(