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 ;
+}
+
+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
@@ -7,6 +7,7 @@
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 {
createTooltip,
type TooltipParams,
@@ -15,6 +16,7 @@
} from '../tooltip/tooltip.react.js';
import type { ChatMultimediaMessageInfoItem } from '../types/chat-types.js';
import type { VerticalBounds } from '../types/layout-types.js';
+import { useNavigateToPinModal } from '../utils/toggle-pin-utils.js';
export type MultimediaMessageTooltipModalParams = TooltipParams<{
+item: ChatMultimediaMessageInfoItem,
@@ -26,7 +28,10 @@
): React.Node {
const { route, tooltipItem: TooltipItem } = props;
- const onPressTogglePin = React.useCallback(() => {}, []);
+ const overlayContext = React.useContext(OverlayContext);
+
+ const onPressTogglePin = useNavigateToPinModal(overlayContext, route);
+
const renderPinIcon = React.useCallback(
style => ,
[],
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
@@ -13,6 +13,7 @@
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 {
createTooltip,
type TooltipParams,
@@ -21,6 +22,7 @@
} from '../tooltip/tooltip.react.js';
import type { ChatTextMessageInfoItemWithHeight } from '../types/chat-types.js';
import { exitEditAlert } from '../utils/edit-messages-utils.js';
+import { useNavigateToPinModal } from '../utils/toggle-pin-utils.js';
export type TextMessageTooltipModalParams = TooltipParams<{
+item: ChatTextMessageInfoItemWithHeight,
@@ -33,6 +35,7 @@
): React.Node {
const { route, tooltipItem: TooltipItem } = props;
+ const overlayContext = React.useContext(OverlayContext);
const inputState = React.useContext(InputStateContext);
const { text } = route.params.item.messageInfo;
const onPressReply = React.useCallback(() => {
@@ -84,7 +87,8 @@
[],
);
- const onPressTogglePin = React.useCallback(() => {}, []);
+ const onPressTogglePin = useNavigateToPinModal(overlayContext, route);
+
const renderPinIcon = React.useCallback(
style => ,
[],
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,194 @@
+// @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(() => {
+ // The if / else if / else conditional is for Flow
+ if (item.messageShapeType === 'robotext') {
+ return item;
+ } else if (item.messageShapeType === 'multimedia') {
+ return {
+ ...item,
+ threadCreatedFromMessage: undefined,
+ reactions: {},
+ startsConversation: false,
+ messageInfo: {
+ ...item.messageInfo,
+ creator: {
+ ...item.messageInfo.creator,
+ isViewer: false,
+ },
+ },
+ };
+ } else {
+ return {
+ ...item,
+ threadCreatedFromMessage: undefined,
+ reactions: {},
+ startsConversation: false,
+ messageInfo: {
+ ...item.messageInfo,
+ creator: {
+ ...item.messageInfo.creator,
+ isViewer: false,
+ },
+ },
+ };
+ }
+ }, [item]);
+
+ const createToggleMessagePinPromise = React.useCallback(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,
+ };
+ }, [callToggleMessagePin, messageInfo.id, modalInfo.action]);
+
+ const onPress = React.useCallback(() => {
+ dispatchActionPromise(
+ toggleMessagePinActionTypes,
+ createToggleMessagePinPromise(),
+ );
+
+ navigation.goBack();
+ }, [createToggleMessagePinPromise, dispatchActionPromise, navigation]);
+
+ const onCancel = React.useCallback(() => {
+ navigation.goBack();
+ }, [navigation]);
+
+ return (
+
+ {modalInfo.name}
+
+ {modalInfo.confirmationText}
+
+
+
+
+
+
+
+ );
+}
+
+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: 'modalButtonLabel',
+ },
+};
+
+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}
/>
+
{pushHandler}
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';
@@ -110,6 +112,11 @@
| typeof MultimediaMessageTooltipModalRouteName
| typeof TextMessageTooltipModalRouteName;
+export const PinnableMessageTooltipRouteNames = [
+ TextMessageTooltipModalRouteName,
+ MultimediaMessageTooltipModalRouteName,
+];
+
export type TooltipModalParamList = {
+MultimediaMessageTooltipModal: MultimediaMessageTooltipModalParams,
+TextMessageTooltipModal: TextMessageTooltipModalParams,
@@ -126,6 +133,7 @@
+UserAvatarCameraModal: void,
+ThreadAvatarCameraModal: ThreadAvatarCameraModalParams,
+VideoPlaybackModal: VideoPlaybackModalParams,
+ +TogglePinModal: TogglePinModalParams,
...TooltipModalParamList,
};
diff --git a/native/utils/toggle-pin-utils.js b/native/utils/toggle-pin-utils.js
new file mode 100644
--- /dev/null
+++ b/native/utils/toggle-pin-utils.js
@@ -0,0 +1,59 @@
+// @flow
+
+import { StackActions, useNavigation } from '@react-navigation/native';
+import * as React from 'react';
+
+import type { OverlayContextType } from '../navigation/overlay-context.js';
+import {
+ type NavigationRoute,
+ PinnableMessageTooltipRouteNames,
+ TogglePinModalRouteName,
+} from '../navigation/route-names.js';
+
+function useNavigateToPinModal(
+ overlayContext: ?OverlayContextType,
+ route:
+ | NavigationRoute<'TextMessageTooltipModal'>
+ | NavigationRoute<'MultimediaMessageTooltipModal'>,
+): () => mixed {
+ const navigation = useNavigation();
+
+ const { params } = route;
+ const { item } = params;
+ const { threadInfo } = item;
+
+ return React.useCallback(() => {
+ // Since 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
+ // TextMessageTooltipModal, 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 there is not a valid routeKey or the most recent routeName is
+ // not included in PinnableMessageTooltipRouteNames, we want to
+ // just navigate to the toggle pin modal as normal.
+ if (!routeKey || !PinnableMessageTooltipRouteNames.includes(routeName)) {
+ navigation.navigate(TogglePinModalRouteName, {
+ threadInfo,
+ item,
+ });
+ return;
+ }
+
+ // Otherwise, we want to replace the tooltip overlay with the pin modal.
+ navigation.dispatch({
+ ...StackActions.replace(TogglePinModalRouteName, {
+ threadInfo,
+ item,
+ }),
+ source: routeKey,
+ });
+ }, [navigation, overlayContext, threadInfo, item]);
+}
+
+export { useNavigateToPinModal };