From a9258543bbcb18958fa950cbcaf263c146906aae Mon Sep 17 00:00:00 2001 From: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:18:35 +0530 Subject: [PATCH] Ensured BoR post actions matches those in webapp (#9362) * Ensured VBoR post actions matches those in webapp * Updated tests * Fixed a value * Review fixes --- .../post_list/post/header/header.tsx | 2 +- app/screens/post_options/index.test.tsx | 45 +++++++++++++++---- app/screens/post_options/index.ts | 13 ++++-- app/screens/post_options/post_options.tsx | 15 ++++--- app/utils/bor.test.ts | 6 +-- app/utils/bor.ts | 4 +- 6 files changed, 63 insertions(+), 22 deletions(-) diff --git a/app/components/post_list/post/header/header.tsx b/app/components/post_list/post/header/header.tsx index a78ef9286..8602e1209 100644 --- a/app/components/post_list/post/header/header.tsx +++ b/app/components/post_list/post/header/header.tsx @@ -102,7 +102,7 @@ const Header = (props: HeaderProps) => { const usernameOverride = ensureString(post.props?.override_username); const isUnrevealedPost = useMemo(() => isUnrevealedBoRPost(post), [post, post.metadata?.expire_at]); - const ownBoRPost = useMemo(() => isOwnBoRPost(post, currentUser), [currentUser, post]); + const ownBoRPost = useMemo(() => isOwnBoRPost(post, currentUser?.id), [currentUser?.id, post]); const showBoRIcon = isUnrevealedPost || ownBoRPost; const borExpireAt = post.metadata?.expire_at; const serverUrl = useServerUrl(); diff --git a/app/screens/post_options/index.test.tsx b/app/screens/post_options/index.test.tsx index d23d5f48f..9b6b228f3 100644 --- a/app/screens/post_options/index.test.tsx +++ b/app/screens/post_options/index.test.tsx @@ -28,7 +28,38 @@ describe('PostOptions', () => { database = server.database; }); - it('should show limited options for unrevealed BoR post', async () => { + it('should show limited options for own BoR post', async () => { + const unrevealedBoRPost = TestHelper.fakePostModel({ + type: PostTypes.BURN_ON_READ, + channelId: TestHelper.basicChannel!.id, + userId: TestHelper.basicUser!.id, + }); + + renderWithEverything( + , + {database}, + ); + + await waitFor(() => { + expect(screen.queryByText('Save')).toBeVisible(); + }); + + expect(screen.queryByText('Copy Link')).toBeVisible(); + + expect(screen.queryByText('Pin to Channel')).not.toBeVisible(); + expect(screen.queryByText('Copy Text')).not.toBeVisible(); + expect(screen.queryByText('Edit')).not.toBeVisible(); + expect(screen.queryByText('Reply')).not.toBeVisible(); + expect(screen.queryByText('Follow Message')).not.toBeVisible(); + }); + + it('should show limited options for received unrevealed BoR post', async () => { const unrevealedBoRPost = TestHelper.fakePostModel({ type: PostTypes.BURN_ON_READ, channelId: TestHelper.basicChannel!.id, @@ -54,9 +85,8 @@ describe('PostOptions', () => { expect(screen.getByText('Mark as Unread')).toBeVisible(); }); - expect(screen.queryByText('Copy Link')).toBeVisible(); - expect(screen.queryByText('Save')).toBeVisible(); - + expect(screen.queryByText('Copy Link')).not.toBeVisible(); + expect(screen.queryByText('Save')).not.toBeVisible(); expect(screen.queryByText('Pin to Channel')).not.toBeVisible(); expect(screen.queryByText('Copy Text')).not.toBeVisible(); expect(screen.queryByText('Edit')).not.toBeVisible(); @@ -64,7 +94,7 @@ describe('PostOptions', () => { expect(screen.queryByText('Follow Message')).not.toBeVisible(); }); - it('should show limited options for revealed BoR post', async () => { + it('should show limited options for someone else\'s revealed BoR post', async () => { const unrevealedBoRPost = TestHelper.fakePostModel({ type: PostTypes.BURN_ON_READ, channelId: TestHelper.basicChannel!.id, @@ -91,11 +121,10 @@ describe('PostOptions', () => { await waitFor(() => { expect(screen.getByText('Mark as Unread')).toBeVisible(); + expect(screen.queryByText('Save')).toBeVisible(); }); - expect(screen.queryByText('Copy Link')).toBeVisible(); - expect(screen.queryByText('Save')).toBeVisible(); - + expect(screen.queryByText('Copy Link')).not.toBeVisible(); expect(screen.queryByText('Copy Text')).not.toBeVisible(); expect(screen.queryByText('Pin to Channel')).not.toBeVisible(); expect(screen.queryByText('Edit')).not.toBeVisible(); diff --git a/app/screens/post_options/index.ts b/app/screens/post_options/index.ts index ccbd2a09c..8d270fb74 100644 --- a/app/screens/post_options/index.ts +++ b/app/screens/post_options/index.ts @@ -3,7 +3,7 @@ import {withDatabase, withObservables} from '@nozbe/watermelondb/react'; import {combineLatest, of as of$, Observable} from 'rxjs'; -import {switchMap} from 'rxjs/operators'; +import {distinctUntilChanged, switchMap} from 'rxjs/operators'; import {Permissions, Post, Screens} from '@constants'; import {AppBindingLocations} from '@constants/apps'; @@ -146,14 +146,20 @@ const enhanced = withObservables([], ({combinedPost, post, showAddReaction, sour }), ); - const canDelete = combineLatest([canDeletePostPermission, channelIsArchived, channelIsReadOnly, canPostPermission]).pipe(switchMap(([permission, isArchived, isReadOnly, canPost]) => { - return of$(permission && !isArchived && !isReadOnly && canPost); + const canDelete = combineLatest([canDeletePostPermission, channelIsArchived, channelIsReadOnly, canPostPermission, currentUser]).pipe(switchMap(([permission, isArchived, isReadOnly, canPost, user]) => { + const canDeleteBoRPost = borPost ? post.userId === user?.id : true; + return of$(permission && !isArchived && !isReadOnly && canPost && canDeleteBoRPost); })); const thread = observeIsCRTEnabled(database).pipe( switchMap((enabled) => (enabled ? observeThreadById(database, post.id) : of$(undefined))), ); + const currentUserId = currentUser.pipe( + switchMap((u) => of$(u?.id)), + distinctUntilChanged(), + ); + return { canMarkAsUnread, canAddReaction, @@ -167,6 +173,7 @@ const enhanced = withObservables([], ({combinedPost, post, showAddReaction, sour thread, bindings, isBoRPost: of$(borPost), + currentUserId, }; }); diff --git a/app/screens/post_options/post_options.tsx b/app/screens/post_options/post_options.tsx index a9ef4c568..6b225a13c 100644 --- a/app/screens/post_options/post_options.tsx +++ b/app/screens/post_options/post_options.tsx @@ -16,6 +16,7 @@ import {useIsTablet} from '@hooks/device'; import useNavButtonPressed from '@hooks/navigation_button_pressed'; import BottomSheet from '@screens/bottom_sheet'; import {dismissBottomSheet} from '@screens/navigation'; +import {isOwnBoRPost, isUnrevealedBoRPost} from '@utils/bor'; import {bottomSheetSnapPoint} from '@utils/helpers'; import {isSystemMessage} from '@utils/post'; @@ -48,13 +49,14 @@ type PostOptionsProps = { bindings: AppBinding[]; serverUrl: string; isBoRPost?: boolean; + currentUserId?: string; }; const PostOptions = ({ canAddReaction, canDelete, canEdit, canMarkAsUnread, canPin, canReply, combinedPost, componentId, isSaved, sourceScreen, post, thread, bindings, serverUrl, - isBoRPost, + isBoRPost, currentUserId, }: PostOptionsProps) => { const managedConfig = useManagedConfig(); const isTablet = useIsTablet(); @@ -69,9 +71,12 @@ const PostOptions = ({ const isSystemPost = isSystemMessage(post); - const canCopyPermalink = !isSystemPost && managedConfig?.copyAndPasteProtection !== 'true'; + const canCopyBoRPostPermalink = isBoRPost ? post.userId === currentUserId : true; + const canCopyPermalink = !isSystemPost && managedConfig?.copyAndPasteProtection !== 'true' && canCopyBoRPostPermalink; const canCopyText = canCopyPermalink && post.message && !isBoRPost; + const canSavePost = !isSystemPost && (!isUnrevealedBoRPost(post) || isOwnBoRPost(post, currentUserId)); + const shouldRenderFollow = !(sourceScreen !== Screens.CHANNEL || !thread); const shouldShowBindings = bindings.length > 0 && !isSystemPost; @@ -79,7 +84,7 @@ const PostOptions = ({ const items: Array = [1]; const optionsCount = [ canCopyPermalink, canCopyText, canDelete, canEdit, - canMarkAsUnread, canPin, canReply, !isSystemPost, shouldRenderFollow, + canMarkAsUnread, canPin, canReply, canSavePost, shouldRenderFollow, ].reduce((acc, v) => { return v ? acc + 1 : acc; }, 0) + (shouldShowBindings ? 0.5 : 0); @@ -94,7 +99,7 @@ const PostOptions = ({ }, [ canAddReaction, canCopyPermalink, canCopyText, canDelete, canEdit, shouldRenderFollow, shouldShowBindings, - canMarkAsUnread, canPin, canReply, isSystemPost, + canMarkAsUnread, canPin, canReply, canSavePost, ]); const renderContent = () => { @@ -136,7 +141,7 @@ const PostOptions = ({ sourceScreen={sourceScreen} /> } - {!isSystemPost && + {canSavePost && { userId: 'user123', } as PostModel; - expect(isOwnBoRPost(ownBorPost, mockUser)).toBe(true); + expect(isOwnBoRPost(ownBorPost, mockUser.id)).toBe(true); }); it('should return false for BoR posts not owned by current user', () => { @@ -109,7 +109,7 @@ describe('BoR utility functions', () => { userId: 'user456', } as PostModel; - expect(isOwnBoRPost(othersBorPost, mockUser)).toBe(false); + expect(isOwnBoRPost(othersBorPost, mockUser.id)).toBe(false); }); it('should return false for non-BoR posts', () => { @@ -118,7 +118,7 @@ describe('BoR utility functions', () => { userId: 'user123', } as PostModel; - expect(isOwnBoRPost(ownRegularPost, mockUser)).toBe(false); + expect(isOwnBoRPost(ownRegularPost, mockUser.id)).toBe(false); }); it('should return false when no current user is provided', () => { diff --git a/app/utils/bor.ts b/app/utils/bor.ts index d00d9229f..19c04bb52 100644 --- a/app/utils/bor.ts +++ b/app/utils/bor.ts @@ -17,8 +17,8 @@ export function isUnrevealedBoRPost(post: Post | PostModel): boolean { return isBoRPost(post) && Boolean(!post.metadata?.expire_at); } -export function isOwnBoRPost(post: PostModel, currentUser?: UserModel): boolean { - return isBoRPost(post) && Boolean(currentUser && post.userId === currentUser.id); +export function isOwnBoRPost(post: PostModel, currentUserId?: UserModel['id']): boolean { + return isBoRPost(post) && post.userId === currentUserId; } function isBoRPostExpiredForMe(post: PostModel): boolean {