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 };