diff --git a/src/components/comments/container/commentsContainer.tsx b/src/components/comments/container/commentsContainer.tsx index 2d5056be06..1587663924 100644 --- a/src/components/comments/container/commentsContainer.tsx +++ b/src/components/comments/container/commentsContainer.tsx @@ -1,17 +1,13 @@ import React, { useState, useEffect } from 'react'; -import { Platform } from 'react-native'; import { connect } from 'react-redux'; import { injectIntl } from 'react-intl'; import get from 'lodash/get'; -import { postBodySummary } from '@ecency/render-helper'; import { useNavigation } from '@react-navigation/native'; import { SheetManager } from 'react-native-actions-sheet'; import { getDiscussionsQueryOptions, useDeleteComment } from '@ecency/sdk'; import { useQueryClient } from '@tanstack/react-query'; // Services and Actions -import { writeToClipboard } from '../../../utils/clipboard'; -import { stripCategoryFromPostPath } from '../../../utils/post'; import { toastNotification } from '../../../redux/actions/uiAction'; // Constants @@ -34,7 +30,6 @@ const CommentsContainer = ({ currentAccount, comments, dispatch, - intl, commentCount, isLoggedIn, commentNumber, @@ -274,32 +269,6 @@ const CommentsContainer = ({ }); }; - const _handleOnPressCommentMenu = (index, selectedComment) => { - const _showCopiedToast = () => { - dispatch( - toastNotification( - intl.formatMessage({ - id: 'alert.copied', - }), - ), - ); - }; - - if (index === 0) { - // Normalize legacy `//@author/permlink…` to the canonical - // `/@author/permlink…` form that ecency.com now treats as canonical - // (the legacy form is 302'd to this one). - const _commentPath = stripCategoryFromPostPath(get(selectedComment, 'url')); - writeToClipboard(`https://ecency.com${_commentPath}`).then(_showCopiedToast); - } - if (index === 1) { - const body = postBodySummary(selectedComment.markdownBody, null, Platform.OS); - writeToClipboard(body).then(_showCopiedToast); - } else if (index === 2) { - _openReplyThread(selectedComment); - } - }; - return ( { - const [selectedComment, setSelectedComment] = useState(null); const intl = useIntl(); - const commentMenu = useRef(); + // Surfaces that pass `handleOnOptionsPress` (waves) route to their own sheet. + // Everywhere else used to fall back to a four-item menu with no delete, edit, + // report or moderation action; it now gets the same sheet the post detail + // screen uses. + const postOptionsModalRef = useRef(null); const upvotePopoverRef = useRef(); const postInteractionRef = useRef(null); const _openCommentMenu = (item) => { if (handleOnOptionsPress) { handleOnOptionsPress(item); - } else if (commentMenu.current) { - setSelectedComment(item); - commentMenu.current.show(); + } else if (postOptionsModalRef.current) { + postOptionsModalRef.current.show(item); } }; + // Without this the sheet falls back to its own delete, which calls + // navigation.goBack() and would pop the profile or bot-comments screen the + // list is embedded in. It would also skip the in-place list removal and, on + // waves, the container's wave-specific delete path. + const _handleDeleteFromMenu = (item) => + handleDeleteComment( + item.permlink, + item.parent_permlink, + item.parent_author, + item.root_author, + item.root_permlink, + ); + const _openReplyThread = (item) => { if (item && openReplyThread) { openReplyThread(item); @@ -73,11 +86,6 @@ const CommentsView = ({ } }; - const _onMenuItemPress = (index) => { - handleOnPressCommentMenu(index, selectedComment); - setSelectedComment(null); - }; - const _onUpvotePress = ({ content, sourceRef, showPayoutDetails, onVotingStart }) => { if (upvotePopoverRef.current) { const postType = isWavesHost(content.parent_author) ? PostTypes.WAVE : PostTypes.COMMENT; @@ -92,13 +100,6 @@ const CommentsView = ({ } }; - const menuItems = [ - intl.formatMessage({ id: 'post.copy_link' }), - intl.formatMessage({ id: 'post.copy_text' }), - intl.formatMessage({ id: 'post.open_thread' }), - intl.formatMessage({ id: 'alert.cancel' }), - ]; - if (!hideManyCommentsButton && hasManyComments) { return ( {!handleOnOptionsPress && ( - )} diff --git a/src/components/postOptionsModal/container/postOptionsModal.tsx b/src/components/postOptionsModal/container/postOptionsModal.tsx index 95ad41bb4f..77dcafe6a4 100644 --- a/src/components/postOptionsModal/container/postOptionsModal.tsx +++ b/src/components/postOptionsModal/container/postOptionsModal.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'; -import { Alert, Share, Text, TouchableHighlight } from 'react-native'; +import { Alert, Platform, Share, Text, TouchableHighlight } from 'react-native'; import { useIntl } from 'react-intl'; import get from 'lodash/get'; import EStyleSheet from 'react-native-extended-stylesheet'; @@ -16,6 +16,7 @@ import { parseProfileMetadata, useDeleteComment, } from '@ecency/sdk'; +import { postBodySummary } from '@ecency/render-helper'; import { useAuthContext } from '../../../providers/sdk'; import { useReblogMutation, @@ -74,9 +75,18 @@ interface Props { * disappears from the feed. */ onDelete?: (content: any) => void | Promise; + /** + * Optional thread handler. When provided, the "open-thread" action is offered + * and delegates here. Only comment surfaces can open a thread, so the option + * is hidden wherever this is absent. + */ + onOpenThread?: (content: any) => void; } -const PostOptionsModal = ({ pageType, isWave, isVisibleTranslateModal, onDelete }: Props, ref) => { +const PostOptionsModal = ( + { pageType, isWave, isVisibleTranslateModal, onDelete, onOpenThread }: Props, + ref, +) => { const intl = useIntl(); const dispatch = useAppDispatch(); const navigation = useNavigation(); @@ -235,6 +245,14 @@ const PostOptionsModal = ({ pageType, isWave, isVisibleTranslateModal, onDelete // offer "unmute" on posts no moderator ever muted. const _isMutedInCommunity = !!content && !!content.stats?.gray; + // Carried over from the legacy comment menu, so both are scoped to comments + // rather than widening the post detail sheet. A comment is anything with a + // parent; copying the text also needs a body to copy, and opening a thread + // only means something where a handler was given. + const _isCommentContent = !!content && (content.depth > 0 || !!content.parent_author); + const _canCopyText = _isCommentContent && !!content?.markdownBody; + const _canOpenThread = _isCommentContent && !!onOpenThread; + // check if post can be deleted // Hive's on-chain rule is: no children AND no net positive rshares. Using // `active_votes.length` was stricter than the chain (a self-vote or a @@ -301,6 +319,10 @@ const PostOptionsModal = ({ pageType, isWave, isVisibleTranslateModal, onDelete return _canMuteCommunityPost && !_isMutedInCommunity; case 'unmute-post': return _canMuteCommunityPost && _isMutedInCommunity; + case 'copy-text': + return _canCopyText; + case 'open-thread': + return _canOpenThread; case 'translate': return isVisibleTranslateModal; case 'delete-post': @@ -830,6 +852,29 @@ const PostOptionsModal = ({ pageType, isWave, isVisibleTranslateModal, onDelete }, 300); break; } + case 'copy-text': { + // The legacy comment menu copied a plain-text summary rather than raw + // markdown, so links and images do not come through as syntax. + // writeToClipboard returns false for empty text, and the summary can be + // empty where the body is only an image or markup, so the success toast + // has to follow the result rather than the attempt. + const _body = postBodySummary(content.markdownBody, null, Platform.OS); + const _copied = await writeToClipboard(_body); + if (!_copied) { + break; + } + alertTimer.current = setTimeout(() => { + dispatch(toastNotification(intl.formatMessage({ id: 'alert.copied' }))); + alertTimer.current = null; + }, 300); + break; + } + case 'open-thread': + // Deferred like the other cases that leave this sheet, so the + // navigation is not swallowed by the sheet's own hide animation. + await delay(700); + onOpenThread?.(content); + break; case 'reblog': _reblog(false); diff --git a/src/config/locales/en-US.json b/src/config/locales/en-US.json index 162d262b4d..3ccf2276b8 100644 --- a/src/config/locales/en-US.json +++ b/src/config/locales/en-US.json @@ -238,6 +238,8 @@ "cross-post": "Cross Post", "pin-blog": "Pin to blog", "copy": "copy link", + "copy-text": "copy text", + "open-thread": "open thread", "unpin-community": "Unpin from community", "mute-post": "Mute post", "unmute-post": "Unmute post", diff --git a/src/constants/options/post.ts b/src/constants/options/post.ts index f85e63b237..d13448d5f8 100644 --- a/src/constants/options/post.ts +++ b/src/constants/options/post.ts @@ -2,6 +2,11 @@ export default [ 'cross-post', 'promote', 'copy', + // Comment surfaces only. Carried over from the legacy comment menu that + // PostOptionsModal replaced, which offered copy-link, copy-text and + // open-thread and nothing else. + 'copy-text', + 'open-thread', 'reply', 'translate', 'reblog',