mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(mobile): the chat's Working line uses desktop's spinner (#30008)
* fix(mobile): the chat's Working line uses desktop's spinner (turning circle, hourglass under reduced motion) * fix(mobile): keep working animations off interaction queue
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { Animated } from 'react-native'
|
||||
import { Hourglass, LoaderCircle } from 'lucide-react-native'
|
||||
import { useReducedMotionEnabled } from '../platform/use-reduced-motion'
|
||||
import { useWorkingRingRotation } from './use-working-ring-rotation'
|
||||
|
||||
/** Desktop's LoadingSpinner: the same turning circle, once per second.
|
||||
* Why: a spinner frozen by reduced motion reads as stuck, so the still state is
|
||||
* a different icon that says "working" without moving. */
|
||||
export function LoadingSpinner({
|
||||
size,
|
||||
color
|
||||
}: {
|
||||
size: number
|
||||
color: string
|
||||
}): React.JSX.Element {
|
||||
const reducedMotion = useReducedMotionEnabled()
|
||||
const rotate = useWorkingRingRotation(reducedMotion === false)
|
||||
if (reducedMotion) {
|
||||
return <Hourglass size={size} color={color} strokeWidth={2} />
|
||||
}
|
||||
return (
|
||||
<Animated.View style={{ transform: [{ rotate }] }}>
|
||||
<LoaderCircle size={size} color={color} strokeWidth={2} />
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
@@ -88,7 +88,7 @@ describe('mobile monitoring indicators', () => {
|
||||
expect(animationLoop).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('keeps the native driver for both working rings on native', async () => {
|
||||
it('keeps both native working rings off the interaction queue', async () => {
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
createElement('View', null, [
|
||||
@@ -100,7 +100,10 @@ describe('mobile monitoring indicators', () => {
|
||||
|
||||
expect(animationTiming).toHaveBeenCalledTimes(2)
|
||||
for (const call of animationTiming.mock.calls) {
|
||||
expect(call).toEqual([expect.anything(), expect.objectContaining({ useNativeDriver: true })])
|
||||
expect(call).toEqual([
|
||||
expect.anything(),
|
||||
expect.objectContaining({ useNativeDriver: true, isInteraction: false })
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ export function useWorkingRingRotation(active: boolean): Animated.AnimatedInterp
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
easing: Easing.linear,
|
||||
// Decorative loops must not hold up interaction-scheduled list rendering.
|
||||
isInteraction: false,
|
||||
// Why: web has no native driver, so a loop over a native-driver timing plays it once.
|
||||
useNativeDriver: Platform.OS !== 'web'
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ vi.mock('react-native', async () => {
|
||||
}
|
||||
})
|
||||
vi.mock('lucide-react-native', () => ({ ChevronRight: 'ChevronRight' }))
|
||||
vi.mock('../components/LoadingSpinner', () => ({ LoadingSpinner: 'LoadingSpinner' }))
|
||||
vi.mock('./MobileNativeChatReasoningRow', () => ({
|
||||
MobileNativeChatReasoningBody: 'ReasoningBody'
|
||||
}))
|
||||
|
||||
@@ -28,6 +28,7 @@ vi.mock('react-native', async () => {
|
||||
}
|
||||
})
|
||||
vi.mock('lucide-react-native', () => ({ ChevronRight: 'ChevronRight' }))
|
||||
vi.mock('../components/LoadingSpinner', () => ({ LoadingSpinner: 'LoadingSpinner' }))
|
||||
vi.mock('./MobileNativeChatReasoningRow', () => ({
|
||||
MobileNativeChatReasoningBody: 'ReasoningBody'
|
||||
}))
|
||||
@@ -96,7 +97,7 @@ describe('MobileNativeChatLiveLine', () => {
|
||||
it('reads "Thinking" beside one spinner while the turn reasons', () => {
|
||||
const root = render()
|
||||
expect(labels(root)).toEqual(['Thinking'])
|
||||
expect(byType(root, 'ActivityIndicator')).toHaveLength(1)
|
||||
expect(byType(root, 'LoadingSpinner')).toHaveLength(1)
|
||||
})
|
||||
|
||||
// The bar owns the clock; the tail line never repeats it.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useContext } from 'react'
|
||||
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { ChevronRight } from 'lucide-react-native'
|
||||
import { nativeChatReasoningDisclosureKey } from '../../../src/shared/native-chat-reasoning-row'
|
||||
import { formatNativeChatActiveTurnLabel } from '../../../src/shared/native-chat-turn-status'
|
||||
import { INLINE_TEXT_SELECTION } from '../components/inline-text-selection'
|
||||
import { LoadingSpinner } from '../components/LoadingSpinner'
|
||||
import { colors, spacing, typography } from '../theme/mobile-theme'
|
||||
import { MobileNativeChatReasoningBody } from './MobileNativeChatReasoningRow'
|
||||
import { MobileNativeChatRowOverlaysContext } from './mobile-native-chat-row-overlays'
|
||||
@@ -49,7 +50,7 @@ export function MobileNativeChatLiveLine({
|
||||
accessibilityLiveRegion="polite"
|
||||
>
|
||||
<View style={styles.glyph}>
|
||||
<ActivityIndicator size="small" color={colors.textMuted} />
|
||||
<LoadingSpinner size={15} color={colors.textMuted} />
|
||||
</View>
|
||||
<Text style={styles.label} numberOfLines={1}>
|
||||
{label}
|
||||
|
||||
Reference in New Issue
Block a user