diff --git a/native/chat/message-result.react.js b/native/chat/message-result.react.js
new file mode 100644
--- /dev/null
+++ b/native/chat/message-result.react.js
@@ -0,0 +1,20 @@
+// @flow
+
+import * as React from 'react';
+import { View } from 'react-native';
+
+import { type ThreadInfo } from 'lib/types/thread-types.js';
+
+import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js';
+
+type MessageResultProps = {
+  +item: ChatMessageInfoItemWithHeight,
+  +threadInfo: ThreadInfo,
+};
+
+/* eslint-disable no-unused-vars */
+function MessageResult(props: MessageResultProps): React.Node {
+  return <View />;
+}
+
+export default MessageResult;
diff --git a/native/chat/multimedia-message-tooltip-modal.react.js b/native/chat/multimedia-message-tooltip-modal.react.js
--- a/native/chat/multimedia-message-tooltip-modal.react.js
+++ b/native/chat/multimedia-message-tooltip-modal.react.js
@@ -1,5 +1,6 @@
 // @flow
 
+import { StackActions, useNavigation } from '@react-navigation/native';
 import * as React from 'react';
 
 import { useOnPressReport } from './message-report-utils.js';
@@ -7,6 +8,11 @@
 import { useAnimatedNavigateToSidebar } from './sidebar-navigation.js';
 import CommIcon from '../components/comm-icon.react.js';
 import SWMansionIcon from '../components/swmansion-icon.react.js';
+import { OverlayContext } from '../navigation/overlay-context.js';
+import {
+  TogglePinModalRouteName,
+  MultimediaMessageTooltipModalRouteName,
+} from '../navigation/route-names.js';
 import {
   createTooltip,
   type TooltipParams,
@@ -25,8 +31,24 @@
   props: TooltipMenuProps<'MultimediaMessageTooltipModal'>,
 ): React.Node {
   const { route, tooltipItem: TooltipItem } = props;
+  const navigation = useNavigation();
+
+  const overlayContext = React.useContext(OverlayContext);
 
-  const onPressTogglePin = React.useCallback(() => {}, []);
+  const onPressTogglePin = React.useCallback(() => {
+    const mostRecentOverlay = overlayContext?.visibleOverlays.slice(-1)[0];
+    const routeName = mostRecentOverlay?.routeName;
+    const routeKey = mostRecentOverlay?.routeKey;
+    if (routeName === MultimediaMessageTooltipModalRouteName) {
+      navigation.dispatch({
+        ...StackActions.replace(TogglePinModalRouteName, {
+          threadInfo: route.params.item.threadInfo,
+          item: route.params.item,
+        }),
+        source: routeKey,
+      });
+    }
+  }, [navigation, overlayContext, route.params.item]);
   const renderPinIcon = React.useCallback(
     style => <CommIcon name="pin" style={style} size={16} />,
     [],
diff --git a/native/chat/text-message-tooltip-modal.react.js b/native/chat/text-message-tooltip-modal.react.js
--- a/native/chat/text-message-tooltip-modal.react.js
+++ b/native/chat/text-message-tooltip-modal.react.js
@@ -1,6 +1,7 @@
 // @flow
 
 import Clipboard from '@react-native-clipboard/clipboard';
+import { useNavigation, StackActions } from '@react-navigation/native';
 import invariant from 'invariant';
 import * as React from 'react';
 
@@ -13,6 +14,11 @@
 import SWMansionIcon from '../components/swmansion-icon.react.js';
 import { InputStateContext } from '../input/input-state.js';
 import { displayActionResultModal } from '../navigation/action-result-modal.js';
+import { OverlayContext } from '../navigation/overlay-context.js';
+import {
+  TextMessageTooltipModalRouteName,
+  TogglePinModalRouteName,
+} from '../navigation/route-names.js';
 import {
   createTooltip,
   type TooltipParams,
@@ -32,7 +38,9 @@
   props: TooltipMenuProps<'TextMessageTooltipModal'>,
 ): React.Node {
   const { route, tooltipItem: TooltipItem } = props;
+  const navigation = useNavigation();
 
+  const overlayContext = React.useContext(OverlayContext);
   const inputState = React.useContext(InputStateContext);
   const { text } = route.params.item.messageInfo;
   const onPressReply = React.useCallback(() => {
@@ -84,7 +92,27 @@
     [],
   );
 
-  const onPressTogglePin = React.useCallback(() => {}, []);
+  const onPressTogglePin = React.useCallback(() => {
+    // If the most recent overlay is the tooltip modal, prior to opening the
+    // toggle pin modal, we want to dismiss it so the overlay is not visible
+    // once the toggle pin modal is closed. This is also necessary with the
+    // TetxMessageTooltipModal, since otherwise the toggle pin modal fails to
+    // render the message since we 'hide' the original message and
+    // show another message on top when the tooltip is active, and this
+    // state carries through into the modal.
+    const mostRecentOverlay = overlayContext?.visibleOverlays.slice(-1)[0];
+    const routeName = mostRecentOverlay?.routeName;
+    const routeKey = mostRecentOverlay?.routeKey;
+    if (routeName === TextMessageTooltipModalRouteName) {
+      navigation.dispatch({
+        ...StackActions.replace(TogglePinModalRouteName, {
+          threadInfo: route.params.item.threadInfo,
+          item: route.params.item,
+        }),
+        source: routeKey,
+      });
+    }
+  }, [navigation, overlayContext, route.params.item]);
   const renderPinIcon = React.useCallback(
     style => <CommIcon name="pin" style={style} size={16} />,
     [],
diff --git a/native/chat/toggle-pin-modal.react.js b/native/chat/toggle-pin-modal.react.js
new file mode 100644
--- /dev/null
+++ b/native/chat/toggle-pin-modal.react.js
@@ -0,0 +1,197 @@
+// @flow
+
+import invariant from 'invariant';
+import * as React from 'react';
+import { Text, View } from 'react-native';
+
+import {
+  toggleMessagePin,
+  toggleMessagePinActionTypes,
+} from 'lib/actions/thread-actions.js';
+import { type ThreadInfo } from 'lib/types/thread-types.js';
+import {
+  useServerCall,
+  useDispatchActionPromise,
+} from 'lib/utils/action-utils.js';
+
+import MessageResult from './message-result.react.js';
+import Button from '../components/button.react.js';
+import Modal from '../components/modal.react.js';
+import type { AppNavigationProp } from '../navigation/app-navigator.react.js';
+import type { NavigationRoute } from '../navigation/route-names.js';
+import { useStyles } from '../themes/colors.js';
+import type { ChatMessageInfoItemWithHeight } from '../types/chat-types';
+
+export type TogglePinModalParams = {
+  +item: ChatMessageInfoItemWithHeight,
+  +threadInfo: ThreadInfo,
+};
+
+type TogglePinModalProps = {
+  +navigation: AppNavigationProp<'TogglePinModal'>,
+  +route: NavigationRoute<'TogglePinModal'>,
+};
+
+function TogglePinModal(props: TogglePinModalProps): React.Node {
+  const { navigation, route } = props;
+  const { item, threadInfo } = route.params;
+  const { messageInfo, isPinned } = item;
+  const styles = useStyles(unboundStyles);
+
+  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',
+        buttonStyle: styles.removePinButton,
+      };
+    }
+
+    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',
+      buttonStyle: styles.pinButton,
+    };
+  }, [isPinned, styles.pinButton, styles.removePinButton]);
+
+  const modifiedItem = React.useMemo(() => {
+    if (item.messageShapeType === 'robotext') {
+      return item;
+    }
+
+    if (item.messageShapeType === 'multimedia') {
+      return {
+        ...item,
+        threadCreatedFromMessage: undefined,
+        reactions: {},
+        startsConversation: false,
+        messageInfo: {
+          ...item.messageInfo,
+          creator: {
+            ...item.messageInfo.creator,
+            isViewer: false,
+          },
+        },
+      };
+    }
+
+    return {
+      ...item,
+      threadCreatedFromMessage: undefined,
+      reactions: {},
+      startsConversation: false,
+      messageInfo: {
+        ...item.messageInfo,
+        creator: {
+          ...item.messageInfo.creator,
+          isViewer: false,
+        },
+      },
+    };
+  }, [item]);
+
+  const onPress = 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(),
+    );
+
+    navigation.goBack();
+  }, [
+    modalInfo,
+    callToggleMessagePin,
+    dispatchActionPromise,
+    messageInfo.id,
+    navigation,
+  ]);
+
+  const onCancel = React.useCallback(() => {
+    navigation.goBack();
+  }, [navigation]);
+
+  return (
+    <Modal modalStyle={styles.modal}>
+      <Text style={styles.modalHeader}>{modalInfo.name}</Text>
+      <Text style={styles.modalConfirmationText}>
+        {modalInfo.confirmationText}
+      </Text>
+      <MessageResult item={modifiedItem} threadInfo={threadInfo} />
+      <View style={styles.buttonsContainer}>
+        <Button style={modalInfo.buttonStyle} onPress={onPress}>
+          <Text style={styles.textColor}>{modalInfo.buttonText}</Text>
+        </Button>
+        <Button style={styles.cancelButton} onPress={onCancel}>
+          <Text style={styles.textColor}>Cancel</Text>
+        </Button>
+      </View>
+    </Modal>
+  );
+}
+
+const unboundStyles = {
+  modal: {
+    backgroundColor: 'modalForeground',
+    borderColor: 'modalForegroundBorder',
+  },
+  modalHeader: {
+    fontSize: 18,
+    color: 'modalForegroundLabel',
+  },
+  modalConfirmationText: {
+    fontSize: 12,
+    color: 'panelBackgroundLabel',
+    marginTop: 4,
+  },
+  buttonsContainer: {
+    flexDirection: 'column',
+    flex: 1,
+    justifyContent: 'flex-end',
+    marginBottom: 0,
+    height: 72,
+    paddingHorizontal: 16,
+  },
+  removePinButton: {
+    borderRadius: 5,
+    height: 48,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: 'vibrantRedButton',
+  },
+  pinButton: {
+    borderRadius: 5,
+    height: 48,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: 'purpleButton',
+  },
+  cancelButton: {
+    borderRadius: 5,
+    height: 48,
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+  textColor: {
+    color: 'white',
+  },
+};
+
+export default TogglePinModal;
diff --git a/native/navigation/app-navigator.react.js b/native/navigation/app-navigator.react.js
--- a/native/navigation/app-navigator.react.js
+++ b/native/navigation/app-navigator.react.js
@@ -28,11 +28,13 @@
   CommunityDrawerNavigatorRouteName,
   type ScreenParamList,
   type OverlayParamList,
+  TogglePinModalRouteName,
 } from './route-names.js';
 import MultimediaMessageTooltipModal from '../chat/multimedia-message-tooltip-modal.react.js';
 import RobotextMessageTooltipModal from '../chat/robotext-message-tooltip-modal.react.js';
 import ThreadSettingsMemberTooltipModal from '../chat/settings/thread-settings-member-tooltip-modal.react.js';
 import TextMessageTooltipModal from '../chat/text-message-tooltip-modal.react.js';
+import TogglePinModal from '../chat/toggle-pin-modal.react.js';
 import KeyboardStateContainer from '../keyboard/keyboard-state-container.react.js';
 import ChatCameraModal from '../media/chat-camera-modal.react.js';
 import ImageModal from '../media/image-modal.react.js';
@@ -151,6 +153,7 @@
           name={VideoPlaybackModalRouteName}
           component={VideoPlaybackModal}
         />
+        <App.Screen name={TogglePinModalRouteName} component={TogglePinModal} />
       </App.Navigator>
       {pushHandler}
     </KeyboardStateContainer>
diff --git a/native/navigation/route-names.js b/native/navigation/route-names.js
--- a/native/navigation/route-names.js
+++ b/native/navigation/route-names.js
@@ -23,6 +23,7 @@
 import type { SidebarListModalParams } from '../chat/sidebar-list-modal.react.js';
 import type { SubchannelListModalParams } from '../chat/subchannels-list-modal.react.js';
 import type { TextMessageTooltipModalParams } from '../chat/text-message-tooltip-modal.react.js';
+import type { TogglePinModalParams } from '../chat/toggle-pin-modal.react.js';
 import type { ChatCameraModalParams } from '../media/chat-camera-modal.react.js';
 import type { ImageModalParams } from '../media/image-modal.react.js';
 import type { ThreadAvatarCameraModalParams } from '../media/thread-avatar-camera-modal.react.js';
@@ -83,6 +84,7 @@
   'ThreadSettingsMemberTooltipModal';
 export const ThreadSettingsRouteName = 'ThreadSettings';
 export const UserAvatarCameraModalRouteName = 'UserAvatarCameraModal';
+export const TogglePinModalRouteName = 'TogglePinModal';
 export const VideoPlaybackModalRouteName = 'VideoPlaybackModal';
 export const TermsAndPrivacyRouteName = 'TermsAndPrivacyModal';
 export const RegistrationRouteName = 'Registration';
@@ -103,6 +105,7 @@
   +MessageReactionsModal: MessageReactionsModalParams,
   +Registration: void,
   +InviteLinkModal: InviteLinkModalParams,
+  +TogglePinModal: TogglePinModalParams,
 };
 
 export type MessageTooltipRouteNames =
@@ -126,6 +129,7 @@
   +UserAvatarCameraModal: void,
   +ThreadAvatarCameraModal: ThreadAvatarCameraModalParams,
   +VideoPlaybackModal: VideoPlaybackModalParams,
+  +TogglePinModal: TogglePinModalParams,
   ...TooltipModalParamList,
 };