diff --git a/native/chat/message-result.react.js b/native/chat/message-result.react.js new file mode 100644 index 000000000..fa7b4bcd7 --- /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 index 3d9c86631..d300b99f0 100644 --- a/native/chat/multimedia-message-tooltip-modal.react.js +++ b/native/chat/multimedia-message-tooltip-modal.react.js @@ -1,94 +1,99 @@ // @flow import * as React from 'react'; import { useOnPressReport } from './message-report-utils.js'; import MultimediaMessageTooltipButton from './multimedia-message-tooltip-button.react.js'; 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, type BaseTooltipProps, type TooltipMenuProps, } 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, +verticalBounds: VerticalBounds, }>; function TooltipMenu( props: TooltipMenuProps<'MultimediaMessageTooltipModal'>, ): 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 => , [], ); const renderUnpinIcon = React.useCallback( style => , [], ); const onPressSidebar = useAnimatedNavigateToSidebar(route.params.item); const renderSidebarIcon = React.useCallback( style => ( ), [], ); const onPressReport = useOnPressReport(route); const renderReportIcon = React.useCallback( style => , [], ); return ( <> ); } const MultimediaMessageTooltipModal: React.ComponentType< BaseTooltipProps<'MultimediaMessageTooltipModal'>, > = createTooltip<'MultimediaMessageTooltipModal'>( MultimediaMessageTooltipButton, TooltipMenu, ); export default MultimediaMessageTooltipModal; diff --git a/native/chat/text-message-tooltip-modal.react.js b/native/chat/text-message-tooltip-modal.react.js index 40675a48b..977c20114 100644 --- a/native/chat/text-message-tooltip-modal.react.js +++ b/native/chat/text-message-tooltip-modal.react.js @@ -1,174 +1,178 @@ // @flow import Clipboard from '@react-native-clipboard/clipboard'; import invariant from 'invariant'; import * as React from 'react'; import { createMessageReply } from 'lib/shared/message-utils.js'; import { useOnPressReport } from './message-report-utils.js'; import { useAnimatedNavigateToSidebar } from './sidebar-navigation.js'; import TextMessageTooltipButton from './text-message-tooltip-button.react.js'; import CommIcon from '../components/comm-icon.react.js'; 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, type BaseTooltipProps, type TooltipMenuProps, } 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, }>; const confirmCopy = () => displayActionResultModal('copied!'); function TooltipMenu( props: TooltipMenuProps<'TextMessageTooltipModal'>, ): 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(() => { invariant( inputState, 'inputState should be set in TextMessageTooltipModal.onPressReply', ); inputState.editInputMessage({ message: createMessageReply(text), mode: 'prepend', }); }, [inputState, text]); const renderReplyIcon = React.useCallback( style => , [], ); const onPressSidebar = useAnimatedNavigateToSidebar(route.params.item); const renderSidebarIcon = React.useCallback( style => ( ), [], ); const { messageInfo } = route.params.item; const onPressEdit = React.useCallback(() => { invariant( inputState, 'inputState should be set in TextMessageTooltipModal.onPressEdit', ); const updateInputBar = () => { inputState.editInputMessage({ message: text, mode: 'replace', }); }; const enterEditMode = () => { inputState.setEditedMessage(messageInfo, updateInputBar); }; if (inputState.editState.editedMessage) { exitEditAlert(enterEditMode); } else { enterEditMode(); } }, [inputState, messageInfo, text]); const renderEditIcon = React.useCallback( style => , [], ); - const onPressTogglePin = React.useCallback(() => {}, []); + const onPressTogglePin = useNavigateToPinModal(overlayContext, route); + const renderPinIcon = React.useCallback( style => , [], ); const renderUnpinIcon = React.useCallback( style => , [], ); const onPressCopy = React.useCallback(() => { Clipboard.setString(text); setTimeout(confirmCopy); }, [text]); const renderCopyIcon = React.useCallback( style => , [], ); const onPressReport = useOnPressReport(route); const renderReportIcon = React.useCallback( style => , [], ); return ( <> ); } const TextMessageTooltipModal: React.ComponentType< BaseTooltipProps<'TextMessageTooltipModal'>, > = createTooltip<'TextMessageTooltipModal'>( TextMessageTooltipButton, TooltipMenu, ); export default TextMessageTooltipModal; diff --git a/native/chat/toggle-pin-modal.react.js b/native/chat/toggle-pin-modal.react.js new file mode 100644 index 000000000..1311bef2b --- /dev/null +++ b/native/chat/toggle-pin-modal.react.js @@ -0,0 +1,198 @@ +// @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, + startsCluster: true, + endsCluster: true, + messageInfo: { + ...item.messageInfo, + creator: { + ...item.messageInfo.creator, + isViewer: false, + }, + }, + }; + } else { + return { + ...item, + threadCreatedFromMessage: undefined, + reactions: {}, + startsConversation: false, + startsCluster: true, + endsCluster: true, + 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: 'modalBackgroundLabel', + 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 index 3aab1dbe0..c934263a9 100644 --- a/native/navigation/app-navigator.react.js +++ b/native/navigation/app-navigator.react.js @@ -1,160 +1,163 @@ // @flow import * as SplashScreen from 'expo-splash-screen'; import * as React from 'react'; import { useSelector } from 'react-redux'; import { PersistGate } from 'redux-persist/es/integration/react.js'; import ActionResultModal from './action-result-modal.react.js'; import { CommunityDrawerNavigator } from './community-drawer-navigator.react.js'; import { createOverlayNavigator } from './overlay-navigator.react.js'; import type { OverlayNavigationProp, OverlayNavigationHelpers, } from './overlay-navigator.react.js'; import type { RootNavigationProp } from './root-navigator.react.js'; import { UserAvatarCameraModalRouteName, ThreadAvatarCameraModalRouteName, ImageModalRouteName, MultimediaMessageTooltipModalRouteName, ActionResultModalRouteName, TextMessageTooltipModalRouteName, ThreadSettingsMemberTooltipModalRouteName, RelationshipListItemTooltipModalRouteName, RobotextMessageTooltipModalRouteName, ChatCameraModalRouteName, VideoPlaybackModalRouteName, 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'; import ThreadAvatarCameraModal from '../media/thread-avatar-camera-modal.react.js'; import UserAvatarCameraModal from '../media/user-avatar-camera-modal.react.js'; import VideoPlaybackModal from '../media/video-playback-modal.react.js'; import RelationshipListItemTooltipModal from '../profile/relationship-list-item-tooltip-modal.react.js'; import PushHandler from '../push/push-handler.react.js'; import { getPersistor } from '../redux/persist.js'; import { RootContext } from '../root-context.js'; import { useLoadCommFonts } from '../themes/fonts.js'; import { waitForInteractions } from '../utils/timers.js'; let splashScreenHasHidden = false; export type AppNavigationProp< RouteName: $Keys = $Keys, > = OverlayNavigationProp; const App = createOverlayNavigator< ScreenParamList, OverlayParamList, OverlayNavigationHelpers, >(); type AppNavigatorProps = { navigation: RootNavigationProp<'App'>, ... }; function AppNavigator(props: AppNavigatorProps): React.Node { const { navigation } = props; const fontsLoaded = useLoadCommFonts(); const rootContext = React.useContext(RootContext); const storeLoadedFromLocalDatabase = useSelector(state => state.storeLoaded); const setNavStateInitialized = rootContext && rootContext.setNavStateInitialized; React.useEffect(() => { setNavStateInitialized && setNavStateInitialized(); }, [setNavStateInitialized]); const [localSplashScreenHasHidden, setLocalSplashScreenHasHidden] = React.useState(splashScreenHasHidden); React.useEffect(() => { if (localSplashScreenHasHidden || !fontsLoaded) { return; } splashScreenHasHidden = true; (async () => { await waitForInteractions(); try { await SplashScreen.hideAsync(); } finally { setLocalSplashScreenHasHidden(true); } })(); }, [localSplashScreenHasHidden, fontsLoaded]); let pushHandler; if (localSplashScreenHasHidden) { pushHandler = ( ); } if (!storeLoadedFromLocalDatabase) { return null; } return ( + {pushHandler} ); } export default AppNavigator; diff --git a/native/navigation/route-names.js b/native/navigation/route-names.js index 5d5228e3a..46b002e61 100644 --- a/native/navigation/route-names.js +++ b/native/navigation/route-names.js @@ -1,218 +1,226 @@ // @flow import type { RouteProp } from '@react-navigation/native'; import type { ActionResultModalParams } from './action-result-modal.react.js'; import type { InviteLinkModalParams } from './invite-link-modal.react'; import type { ConnectEthereumParams } from '../account/registration/connect-ethereum.react.js'; import type { KeyserverSelectionParams } from '../account/registration/keyserver-selection.react.js'; import type { TermsAndPrivacyModalParams } from '../account/terms-and-privacy-modal.react.js'; import type { ThreadPickerModalParams } from '../calendar/thread-picker-modal.react.js'; import type { ComposeSubchannelParams } from '../chat/compose-subchannel.react.js'; import type { FullScreenThreadMediaGalleryParams } from '../chat/fullscreen-thread-media-gallery.react.js'; import type { ImagePasteModalParams } from '../chat/image-paste-modal.react.js'; import type { MessageListParams } from '../chat/message-list-types.js'; import type { MessageReactionsModalParams } from '../chat/message-reactions-modal.react.js'; import type { MultimediaMessageTooltipModalParams } from '../chat/multimedia-message-tooltip-modal.react.js'; import type { RobotextMessageTooltipModalParams } from '../chat/robotext-message-tooltip-modal.react.js'; import type { AddUsersModalParams } from '../chat/settings/add-users-modal.react.js'; import type { ColorSelectorModalParams } from '../chat/settings/color-selector-modal.react.js'; import type { ComposeSubchannelModalParams } from '../chat/settings/compose-subchannel-modal.react.js'; import type { DeleteThreadParams } from '../chat/settings/delete-thread.react.js'; import type { EmojiThreadAvatarCreationParams } from '../chat/settings/emoji-thread-avatar-creation.react.js'; import type { ThreadSettingsMemberTooltipModalParams } from '../chat/settings/thread-settings-member-tooltip-modal.react.js'; import type { ThreadSettingsParams } from '../chat/settings/thread-settings.react.js'; 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'; import type { VideoPlaybackModalParams } from '../media/video-playback-modal.react.js'; import type { CustomServerModalParams } from '../profile/custom-server-modal.react.js'; import type { RelationshipListItemTooltipModalParams } from '../profile/relationship-list-item-tooltip-modal.react.js'; export const ActionResultModalRouteName = 'ActionResultModal'; export const AddUsersModalRouteName = 'AddUsersModal'; export const AppearancePreferencesRouteName = 'AppearancePreferences'; export const AppRouteName = 'App'; export const AppsRouteName = 'Apps'; export const BackgroundChatThreadListRouteName = 'BackgroundChatThreadList'; export const BlockListRouteName = 'BlockList'; export const BuildInfoRouteName = 'BuildInfo'; export const CalendarRouteName = 'Calendar'; export const ChatCameraModalRouteName = 'ChatCameraModal'; export const ChatRouteName = 'Chat'; export const ChatThreadListRouteName = 'ChatThreadList'; export const ColorSelectorModalRouteName = 'ColorSelectorModal'; export const ComposeSubchannelModalRouteName = 'ComposeSubchannelModal'; export const ComposeSubchannelRouteName = 'ComposeSubchannel'; export const CommunityDrawerNavigatorRouteName = 'CommunityDrawerNavigator'; export const CustomServerModalRouteName = 'CustomServerModal'; export const DefaultNotificationsPreferencesRouteName = 'DefaultNotifications'; export const DeleteAccountRouteName = 'DeleteAccount'; export const DeleteThreadRouteName = 'DeleteThread'; export const DevToolsRouteName = 'DevTools'; export const EditPasswordRouteName = 'EditPassword'; export const EmojiThreadAvatarCreationRouteName = 'EmojiThreadAvatarCreation'; export const EmojiUserAvatarCreationRouteName = 'EmojiUserAvatarCreation'; export const FriendListRouteName = 'FriendList'; export const FullScreenThreadMediaGalleryRouteName = 'FullScreenThreadMediaGallery'; export const HomeChatThreadListRouteName = 'HomeChatThreadList'; export const ImageModalRouteName = 'ImageModal'; export const ImagePasteModalRouteName = 'ImagePasteModal'; export const InviteLinkModalRouteName = 'InviteLinkModal'; export const LoggedOutModalRouteName = 'LoggedOutModal'; export const MessageListRouteName = 'MessageList'; export const MessageReactionsModalRouteName = 'MessageReactionsModal'; export const MultimediaMessageTooltipModalRouteName = 'MultimediaMessageTooltipModal'; export const PrivacyPreferencesRouteName = 'PrivacyPreferences'; export const ProfileRouteName = 'Profile'; export const ProfileScreenRouteName = 'ProfileScreen'; export const RelationshipListItemTooltipModalRouteName = 'RelationshipListItemTooltipModal'; export const RobotextMessageTooltipModalRouteName = 'RobotextMessageTooltipModal'; export const SidebarListModalRouteName = 'SidebarListModal'; export const SubchannelsListModalRouteName = 'SubchannelsListModal'; export const TabNavigatorRouteName = 'TabNavigator'; export const TextMessageTooltipModalRouteName = 'TextMessageTooltipModal'; export const ThreadAvatarCameraModalRouteName = 'ThreadAvatarCameraModal'; export const ThreadPickerModalRouteName = 'ThreadPickerModal'; export const ThreadSettingsMemberTooltipModalRouteName = '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'; export const KeyserverSelectionRouteName = 'KeyserverSelection'; export const CoolOrNerdModeSelectionRouteName = 'CoolOrNerdModeSelection'; export const ConnectEthereumRouteName = 'ConnectEthereum'; export type RootParamList = { +LoggedOutModal: void, +App: void, +ThreadPickerModal: ThreadPickerModalParams, +AddUsersModal: AddUsersModalParams, +CustomServerModal: CustomServerModalParams, +ColorSelectorModal: ColorSelectorModalParams, +ComposeSubchannelModal: ComposeSubchannelModalParams, +SidebarListModal: SidebarListModalParams, +ImagePasteModal: ImagePasteModalParams, +TermsAndPrivacyModal: TermsAndPrivacyModalParams, +SubchannelsListModal: SubchannelListModalParams, +MessageReactionsModal: MessageReactionsModalParams, +Registration: void, +InviteLinkModal: InviteLinkModalParams, }; export type MessageTooltipRouteNames = | typeof RobotextMessageTooltipModalRouteName | typeof MultimediaMessageTooltipModalRouteName | typeof TextMessageTooltipModalRouteName; +export const PinnableMessageTooltipRouteNames = [ + TextMessageTooltipModalRouteName, + MultimediaMessageTooltipModalRouteName, +]; + export type TooltipModalParamList = { +MultimediaMessageTooltipModal: MultimediaMessageTooltipModalParams, +TextMessageTooltipModal: TextMessageTooltipModalParams, +ThreadSettingsMemberTooltipModal: ThreadSettingsMemberTooltipModalParams, +RelationshipListItemTooltipModal: RelationshipListItemTooltipModalParams, +RobotextMessageTooltipModal: RobotextMessageTooltipModalParams, }; export type OverlayParamList = { +CommunityDrawerNavigator: void, +ImageModal: ImageModalParams, +ActionResultModal: ActionResultModalParams, +ChatCameraModal: ChatCameraModalParams, +UserAvatarCameraModal: void, +ThreadAvatarCameraModal: ThreadAvatarCameraModalParams, +VideoPlaybackModal: VideoPlaybackModalParams, + +TogglePinModal: TogglePinModalParams, ...TooltipModalParamList, }; export type TabParamList = { +Calendar: void, +Chat: void, +Profile: void, +Apps: void, }; export type ChatParamList = { +ChatThreadList: void, +MessageList: MessageListParams, +ComposeSubchannel: ComposeSubchannelParams, +ThreadSettings: ThreadSettingsParams, +EmojiThreadAvatarCreation: EmojiThreadAvatarCreationParams, +DeleteThread: DeleteThreadParams, +FullScreenThreadMediaGallery: FullScreenThreadMediaGalleryParams, }; export type ChatTopTabsParamList = { +HomeChatThreadList: void, +BackgroundChatThreadList: void, }; export type ProfileParamList = { +ProfileScreen: void, +EmojiUserAvatarCreation: void, +EditPassword: void, +DeleteAccount: void, +BuildInfo: void, +DevTools: void, +AppearancePreferences: void, +PrivacyPreferences: void, +DefaultNotifications: void, +FriendList: void, +BlockList: void, }; export type CommunityDrawerParamList = { +TabNavigator: void }; export type RegistrationParamList = { +CoolOrNerdModeSelection: void, +KeyserverSelection: KeyserverSelectionParams, +ConnectEthereum: ConnectEthereumParams, }; export type ScreenParamList = { ...RootParamList, ...OverlayParamList, ...TabParamList, ...ChatParamList, ...ChatTopTabsParamList, ...ProfileParamList, ...CommunityDrawerParamList, ...RegistrationParamList, }; export type NavigationRoute> = RouteProp; export const accountModals = [LoggedOutModalRouteName, RegistrationRouteName]; export const scrollBlockingModals = [ ImageModalRouteName, MultimediaMessageTooltipModalRouteName, TextMessageTooltipModalRouteName, ThreadSettingsMemberTooltipModalRouteName, RelationshipListItemTooltipModalRouteName, RobotextMessageTooltipModalRouteName, VideoPlaybackModalRouteName, ]; export const chatRootModals = [ AddUsersModalRouteName, ColorSelectorModalRouteName, ComposeSubchannelModalRouteName, ]; export const threadRoutes = [ MessageListRouteName, ThreadSettingsRouteName, DeleteThreadRouteName, ComposeSubchannelRouteName, FullScreenThreadMediaGalleryRouteName, ]; diff --git a/native/utils/toggle-pin-utils.js b/native/utils/toggle-pin-utils.js new file mode 100644 index 000000000..e2cd3b6f6 --- /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 };