diff --git a/web/components/message-result.css b/web/components/message-result.css
new file mode 100644
--- /dev/null
+++ b/web/components/message-result.css
@@ -0,0 +1,28 @@
+.messageContainer {
+  overflow-y: scroll;
+  border: 1px solid var(--pin-message-modal-border-color);
+  border-radius: 7px;
+  max-height: 400px;
+  margin: 16px 32px;
+}
+
+.messageDate {
+  color: var(--chat-timestamp-color);
+  font-size: var(--xs-font-12);
+  padding: 0px 0px 6px 0px;
+  line-height: var(--line-height-text);
+  text-align: left;
+  margin-left: 16px;
+}
+
+.creator {
+  font-size: small;
+  color: var(--shades-white-60);
+  font-size: var(--s-font-14);
+  padding: 4px 24px;
+  text-align: left;
+}
+
+.messageContent {
+  margin-bottom: 1px;
+}
diff --git a/web/components/message-result.react.js b/web/components/message-result.react.js
new file mode 100644
--- /dev/null
+++ b/web/components/message-result.react.js
@@ -0,0 +1,57 @@
+// @flow
+
+import * as React from 'react';
+
+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 { longAbsoluteDate } from 'lib/utils/date-utils.js';
+
+import css from './message-result.css';
+import { MessageListContext } from '../chat/message-list-types.js';
+import Message from '../chat/message.react.js';
+import { useTextMessageRulesFunc } from '../markdown/rules.react.js';
+
+type MessageResultProps = {
+  +item: ChatMessageInfoItem,
+  +threadInfo: ThreadInfo,
+};
+
+function MessageResult(props: MessageResultProps): React.Node {
+  const { item, threadInfo } = props;
+
+  const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadInfo);
+  const messageListContext = React.useMemo(() => {
+    if (!getTextMessageMarkdownRules) {
+      return undefined;
+    }
+    return { getTextMessageMarkdownRules };
+  }, [getTextMessageMarkdownRules]);
+
+  const shouldShowUsername = !item.startsConversation && !item.startsCluster;
+  const username = useStringForUser(
+    shouldShowUsername ? item.messageInfo.creator : null,
+  );
+
+  return (
+    <div className={css.messageContainer}>
+      <div>
+        <div className={css.creator}>{username}</div>
+        <div className={css.messageContent}>
+          <MessageListContext.Provider value={messageListContext}>
+            <Message
+              item={item}
+              threadInfo={threadInfo}
+              key={item.messageInfo.id}
+            />
+          </MessageListContext.Provider>
+        </div>
+        <div className={css.messageDate}>
+          {longAbsoluteDate(item.messageInfo.time)}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+export default MessageResult;
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,30 @@
+.confirmationText {
+  color: var(--pin-message-information-text-color);
+  padding: 16px 32px 0 32px;
+  font-size: small;
+}
+
+.buttonContainer {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-self: center;
+  align-items: stretch;
+  margin-bottom: 16px;
+}
+
+.togglePinButton {
+  margin: 0 32px 0 32px;
+}
+
+.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,129 @@
 // @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 type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js';
 import type { ThreadInfo } from 'lib/types/thread-types.js';
+import {
+  useServerCall,
+  useDispatchActionPromise,
+} from 'lib/utils/action-utils.js';
+
+import css from './toggle-pin-modal.css';
+import Button, { buttonThemes } from '../../components/button.react.js';
+import MessageResult from '../../components/message-result.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 { popModal } = useModalContext();
+
+  const callToggleMessagePin = useServerCall(toggleMessagePin);
+  const dispatchActionPromise = useDispatchActionPromise();
+
+  const modalInfo = React.useMemo(() => {
+    if (isPinned) {
+      return {
+        name: 'Remove Pinned Message',
+        action: 'unpin',
+        confirmationText:
+          'Are you sure you want to remove this pinned message?',
+        buttonText: 'Remove Pinned Message',
+        buttonColor: buttonThemes.danger,
+      };
+    }
+
+    return {
+      name: 'Pin Message',
+      action: 'pin',
+      confirmationText: `You may pin this message to the channel 
+        you are currently viewing. To unpin a message, select the pinned 
+        messages icon in the channel.`,
+      buttonText: 'Pin Message',
+      buttonColor: buttonThemes.standard,
+    };
+  }, [isPinned]);
+
+  // We want to remove inline engagement (threadCreatedFromMessage / reactions)
+  // and the message header (startsConversation). We also want to set isViewer
+  // to false so that the message is left-aligned and uncolored.
+  const modifiedItem = React.useMemo(() => {
+    if (item.messageInfoType !== 'composable') {
+      return item;
+    }
+
+    return {
+      ...item,
+      threadCreatedFromMessage: undefined,
+      reactions: {},
+      startsConversation: false,
+      messageInfo: {
+        ...item.messageInfo,
+        creator: {
+          ...item.messageInfo.creator,
+          isViewer: false,
+        },
+      },
+    };
+  }, [item]);
+
+  const onClick = React.useCallback(() => {
+    const createToggleMessagePinPromise = async () => {
+      invariant(messageInfo.id, 'messageInfo.id should be defined');
+      const result = await callToggleMessagePin({
+        messageID: messageInfo.id,
+        action: modalInfo.action,
+      });
+      return {
+        newMessageInfos: result.newMessageInfos,
+        threadID: result.threadID,
+      };
+    };
+
+    dispatchActionPromise(
+      toggleMessagePinActionTypes,
+      createToggleMessagePinPromise(),
+    );
+    popModal();
+  }, [
+    modalInfo,
+    callToggleMessagePin,
+    dispatchActionPromise,
+    messageInfo.id,
+    popModal,
+  ]);
+
+  return (
+    <Modal name={modalInfo.name} onClose={popModal} size="large">
+      <div className={css.confirmationText}>{modalInfo.confirmationText}</div>
+      <MessageResult item={modifiedItem} threadInfo={threadInfo} />
+      <div className={css.buttonContainer}>
+        <Button
+          variant="filled"
+          className={css.togglePinButton}
+          buttonColor={modalInfo.buttonColor}
+          onClick={onClick}
+        >
+          {modalInfo.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-action-utils.js b/web/utils/tooltip-action-utils.js
--- a/web/utils/tooltip-action-utils.js
+++ b/web/utils/tooltip-action-utils.js
@@ -192,6 +192,8 @@
     isComposableMessageType(messageInfo.type) &&
     threadHasPermission(threadInfo, threadPermissions.MANAGE_PINS);
 
+  const inputState = React.useContext(InputStateContext);
+
   return React.useMemo(() => {
     if (!canTogglePin) {
       return null;
@@ -202,7 +204,11 @@
     const buttonContent = <CommIcon 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 {
@@ -210,7 +216,7 @@
       onClick: onClickTogglePin,
       label: isPinned ? 'Unpin' : 'Pin',
     };
-  }, [canTogglePin, isPinned, pushModal, item, threadInfo]);
+  }, [canTogglePin, inputState, isPinned, pushModal, item, threadInfo]);
 }
 
 function useMessageTooltipActions(