mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 16:02:29 +00:00
fix(mobile): keep the long-press actions sheet on reasoning rows for Android
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import type { ComponentProps, ReactNode } from 'react'
|
||||
import { Pressable, View } from 'react-native'
|
||||
|
||||
/** A message body that opens the actions sheet on long press (Android, which has no inline
|
||||
* selection). Keep the existing responder hierarchy on platforms with inline selection. */
|
||||
export function MobileNativeChatLongPressContent({
|
||||
onLongPress,
|
||||
style,
|
||||
children
|
||||
}: {
|
||||
onLongPress?: () => void
|
||||
style: ComponentProps<typeof View>['style']
|
||||
children: ReactNode
|
||||
}): React.JSX.Element {
|
||||
return onLongPress ? (
|
||||
<Pressable onLongPress={onLongPress} style={style}>
|
||||
{children}
|
||||
</Pressable>
|
||||
) : (
|
||||
<View style={style}>{children}</View>
|
||||
)
|
||||
}
|
||||
@@ -32,6 +32,7 @@ vi.mock('react-native', async () => {
|
||||
vi.mock('expo-clipboard', () => ({ setStringAsync: vi.fn() }))
|
||||
vi.mock('lucide-react-native', () => ({
|
||||
ArrowUp: 'ArrowUp',
|
||||
Brain: 'Brain',
|
||||
ChevronDown: 'ChevronDown',
|
||||
Copy: 'Copy',
|
||||
SquareChevronRight: 'SquareChevronRight',
|
||||
@@ -106,6 +107,36 @@ describe('MobileNativeChatMessage on Android', () => {
|
||||
expect(byType('MessageActionsSheet')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('opens the actions sheet from a long press on an expanded reasoning row, not on its headline', () => {
|
||||
const reasoning: NativeChatMessage = {
|
||||
...message,
|
||||
id: 'r1',
|
||||
role: 'reasoning',
|
||||
blocks: [{ type: 'text', text: 'Weighing two approaches' }],
|
||||
timestamp: 1_000,
|
||||
state: 'completed',
|
||||
completedAt: 4_000
|
||||
}
|
||||
act(() => {
|
||||
renderer = create(createElement(MobileNativeChatMessage, { message: reasoning }))
|
||||
})
|
||||
const [toggle] = byType('Pressable')
|
||||
// A long press on the headline stays a plain toggle tap.
|
||||
expect(toggle!.props.onLongPress).toBeUndefined()
|
||||
act(() => toggle!.props.onPress())
|
||||
|
||||
const body = byType('Pressable').find((node) => node.props.onLongPress !== undefined)
|
||||
expect(typeof body?.props.onLongPress).toBe('function')
|
||||
const [markdown] = byType('MobileMarkdown')
|
||||
expect(markdown!.props.onLongPress).toBe(body!.props.onLongPress)
|
||||
|
||||
act(() => body!.props.onLongPress())
|
||||
const [sheet] = byType('MessageActionsSheet')
|
||||
expect(sheet!.props.message).toBe(reasoning)
|
||||
act(() => sheet!.props.onClose())
|
||||
expect(byType('MessageActionsSheet')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('renders the user bubble without inline selection', () => {
|
||||
act(() => {
|
||||
renderer = create(
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { MobileSelectableText as Text } from '../components/MobileSelectableText'
|
||||
import { memo, useCallback, useState, type ComponentProps, type ReactNode } from 'react'
|
||||
import { Image, Text as NativeText, Pressable, View } from 'react-native'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { Image, Text as NativeText, View } from 'react-native'
|
||||
import { INLINE_TEXT_SELECTION } from '../components/inline-text-selection'
|
||||
import { MobileNativeChatMessageActionsSheet } from './MobileNativeChatMessageActionsSheet'
|
||||
import { MobileNativeChatLongPressContent as Content } from './MobileNativeChatLongPressContent'
|
||||
import { splitNativeChatBlocks } from '../../../src/shared/native-chat-tool-fold'
|
||||
import { selectActiveToolCall } from '../../../src/shared/native-chat-tool-activity'
|
||||
import { isImageRefBlock, isTextBlock } from '../../../src/shared/native-chat-types'
|
||||
@@ -90,25 +91,6 @@ function Prose({
|
||||
return null
|
||||
}
|
||||
|
||||
// Keep the existing responder hierarchy on platforms with inline selection.
|
||||
function Content({
|
||||
onLongPress,
|
||||
style,
|
||||
children
|
||||
}: {
|
||||
onLongPress?: () => void
|
||||
style: ComponentProps<typeof View>['style']
|
||||
children: ReactNode
|
||||
}): React.JSX.Element {
|
||||
return onLongPress ? (
|
||||
<Pressable onLongPress={onLongPress} style={style}>
|
||||
{children}
|
||||
</Pressable>
|
||||
) : (
|
||||
<View style={style}>{children}</View>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileNativeChatMessageImpl({
|
||||
message,
|
||||
toolsExpanded = false,
|
||||
@@ -193,9 +175,16 @@ function MobileNativeChatMessageImpl({
|
||||
markdown={markdown}
|
||||
fontScale={fontScale}
|
||||
onOpenFile={onOpenFile}
|
||||
onLongPress={onLongPress}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
{actionsOpen ? (
|
||||
<MobileNativeChatMessageActionsSheet
|
||||
message={message}
|
||||
onClose={() => setActionsOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
{turnStatusAbove ? null : statusRow}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import type { NativeChatMessage } from '../../../src/shared/native-chat-types'
|
||||
import { MobileMarkdown } from '../components/MobileMarkdown'
|
||||
import { colors } from '../theme/mobile-theme'
|
||||
import { MobileNativeChatLongPressContent } from './MobileNativeChatLongPressContent'
|
||||
import { styles } from './mobile-native-chat-message-styles'
|
||||
|
||||
/** A reasoning row, collapsed to its headline; its text mounts only once opened. Desktop parity:
|
||||
@@ -16,12 +17,15 @@ export function MobileNativeChatReasoningRow({
|
||||
message,
|
||||
markdown,
|
||||
fontScale,
|
||||
onOpenFile
|
||||
onOpenFile,
|
||||
onLongPress
|
||||
}: {
|
||||
message: Pick<NativeChatMessage, 'state' | 'completedAt' | 'timestamp'>
|
||||
markdown: string
|
||||
fontScale: number
|
||||
onOpenFile?: (relativePath: string) => void
|
||||
/** Android only: opens the message's actions sheet, as a long press on any other message does. */
|
||||
onLongPress?: () => void
|
||||
}): React.JSX.Element {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const headline = nativeChatReasoningHeadlineText(nativeChatReasoningHeadline(message))
|
||||
@@ -46,14 +50,15 @@ export function MobileNativeChatReasoningRow({
|
||||
</View>
|
||||
</Pressable>
|
||||
{expanded ? (
|
||||
<View style={styles.reasoning}>
|
||||
<MobileNativeChatLongPressContent onLongPress={onLongPress} style={styles.reasoning}>
|
||||
<MobileMarkdown
|
||||
content={markdown}
|
||||
rangeSelectable
|
||||
textScale={1.25 * fontScale}
|
||||
onOpenFile={onOpenFile}
|
||||
onLongPress={onLongPress}
|
||||
/>
|
||||
</View>
|
||||
</MobileNativeChatLongPressContent>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user