From e4d22ea3baaaeb11acbd9f01e4ba08574fc2e28e Mon Sep 17 00:00:00 2001 From: feruzm Date: Sat, 1 Aug 2026 07:04:14 +0000 Subject: [PATCH 1/2] feat(comments): replace the legacy options menu with the post options sheet Five comment surfaces never used PostOptionsModal. They fell back to the menu in commentsView, which offered four items: copy link, copy text, open thread, cancel. On botComments, commentsDisplay, commentsModal and the profile waves and comments tabs, a comment had no delete, edit, report, pin, translate, bookmark or moderation action at all. The fallback now mounts PostOptionsModal, so every comment surface gets the same actions the post detail screen already had. Surfaces that pass handleOnOptionsPress (waves) keep routing to their own sheet. Two actions the legacy menu had were missing from the sheet, so they are added rather than dropped: - copy-text copies a plain-text summary, so links and images do not come through as markdown syntax, matching what the old menu did. Offered wherever the content has a body. - open-thread is delegated through a new optional onOpenThread prop and is offered only where a handler is given, since only comment surfaces can open a thread. It defers like the other cases that leave the sheet. The dead legacy handler and its now-unused imports are removed from commentsContainer. --- .../comments/container/commentsContainer.tsx | 32 --------------- src/components/comments/view/commentsView.tsx | 40 ++++++------------- .../container/postOptionsModal.tsx | 40 ++++++++++++++++++- src/config/locales/en-US.json | 2 + src/constants/options/post.ts | 5 +++ 5 files changed, 58 insertions(+), 61 deletions(-) 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); } }; @@ -73,11 +73,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 +87,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..e8dd022941 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,11 @@ 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. Copying the text needs a body + // to copy; opening a thread only means something where a handler was given. + const _canCopyText = !!content?.markdownBody; + const _canOpenThread = !!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 +316,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 +849,23 @@ 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. + const _body = postBodySummary(content.markdownBody, null, Platform.OS); + await writeToClipboard(_body); + 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', From a371e95ba22bc2992a0cdf7e86a8721e3341f551 Mon Sep 17 00:00:00 2001 From: feruzm Date: Sat, 1 Aug 2026 07:15:14 +0000 Subject: [PATCH 2/2] fix(comments): delegate delete, and scope the two carried-over actions Three review findings. Delete was falling through to the sheet's own path on every converted surface, which calls navigation.goBack() and would pop the profile or bot-comments screen the list is embedded in. It also skipped the container's in-place list removal and, on waves, the wave-specific delete that updates the infinite-query cache. The sheet now receives an onDelete that forwards to the list's existing handler with the same arguments the inline delete button uses. copy-text and open-thread were documented as comment-only but gated only on a body and a handler, so copy-text would have appeared on the post detail sheet too. Both are now scoped to content with a parent, so post detail is unchanged. The copy-text success toast fired on attempt rather than result. writeToClipboard returns false for empty text, and the plain-text summary can be empty where a comment body is only an image or markup, so that path could report a copy that never happened. --- src/components/comments/view/commentsView.tsx | 14 ++++++++++++++ .../container/postOptionsModal.tsx | 19 ++++++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/components/comments/view/commentsView.tsx b/src/components/comments/view/commentsView.tsx index 23071cc398..b1673c77c6 100644 --- a/src/components/comments/view/commentsView.tsx +++ b/src/components/comments/view/commentsView.tsx @@ -61,6 +61,19 @@ const CommentsView = ({ } }; + // 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); @@ -178,6 +191,7 @@ const CommentsView = ({ ref={postOptionsModalRef} isVisibleTranslateModal={true} onOpenThread={_openReplyThread} + onDelete={_handleDeleteFromMenu} /> )} diff --git a/src/components/postOptionsModal/container/postOptionsModal.tsx b/src/components/postOptionsModal/container/postOptionsModal.tsx index e8dd022941..77dcafe6a4 100644 --- a/src/components/postOptionsModal/container/postOptionsModal.tsx +++ b/src/components/postOptionsModal/container/postOptionsModal.tsx @@ -245,10 +245,13 @@ const PostOptionsModal = ( // offer "unmute" on posts no moderator ever muted. const _isMutedInCommunity = !!content && !!content.stats?.gray; - // Carried over from the legacy comment menu. Copying the text needs a body - // to copy; opening a thread only means something where a handler was given. - const _canCopyText = !!content?.markdownBody; - const _canOpenThread = !!onOpenThread; + // 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 @@ -852,8 +855,14 @@ const PostOptionsModal = ( 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); - await writeToClipboard(_body); + const _copied = await writeToClipboard(_body); + if (!_copied) { + break; + } alertTimer.current = setTimeout(() => { dispatch(toastNotification(intl.formatMessage({ id: 'alert.copied' }))); alertTimer.current = null;