Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/fix-flashlist-recycling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"streamdown-rn": patch
---

Fix FlashList v2 cell recycling bug where messages rendered with wrong content after scrolling

- Fixed `useRef` state persisting across recycled cells by detecting content changes and resetting registry
- Added `AutoSizedImage` component that fetches actual image dimensions for correct aspect ratios
- Fixed TypeScript error where `node.alt` could be `null`
8 changes: 5 additions & 3 deletions apps/debugger-ios/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
"dependencies": {
"@babel/runtime": "^7.28.4",
"@darkresearch/debug-components": "workspace:*",
"@legendapp/list": "^2.0.16",
"streamdown-rn": "workspace:*",
"@expo-google-fonts/geist": "^0.4.1",
"@expo/metro-runtime": "~6.1.2",
"@legendapp/list": "^2.0.16",
"@react-native-vector-icons/lucide": "^12.4.0",
"@react-native/assets-registry": "^0.81.5",
"@shopify/flash-list": "2.0.2",
"expo": "~54.0.25",
"expo-asset": "~12.0.10",
"expo-font": "~14.0.9",
Expand All @@ -28,11 +28,13 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-markdown-display": "^7.0.2",
"react-native-reanimated": "~4.1.5",
"react-native-svg": "^15.15.0",
"react-native-unistyles": "^3.0.18",
"react-native-web": "^0.21.2",
"react-native-worklets": "^0.6.1"
"react-native-worklets": "^0.6.1",
"streamdown-rn": "workspace:*"
},
"devDependencies": {
"@babel/core": "^7.28.5",
Expand Down
85 changes: 50 additions & 35 deletions apps/debugger-ios/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,61 +3,61 @@ import { View, Text, ScrollView, Pressable } from 'react-native';
import { StyleSheet, UnistylesRuntime } from 'react-native-unistyles';
import { StreamdownRN } from 'streamdown-rn';
import { debugComponentRegistry } from '@darkresearch/debug-components';
import { ChatHistoryTest } from './screens/ChatHistoryTest';
import { ListPickerScreen, ListType } from './screens/ListPickerScreen';
import { ChatTestScreen } from './screens/ChatTestScreen';

const WS_URL = 'ws://localhost:3001';

type AppMode = 'debugger' | 'chat-history';
type AppMode =
| { type: 'debugger' }
| { type: 'list-picker' }
| { type: 'chat-test'; listType: ListType };

// Note: Unistyles is configured in unistyles.config.ts (imported by index.js)

export default function App() {
const [mode, setMode] = useState<AppMode>('debugger');
const [mode, setMode] = useState<AppMode>({ type: 'debugger' });
const [content, setContent] = useState('');
const [connected, setConnected] = useState(false);

useEffect(() => {
// Only connect to WebSocket in debugger mode
if (mode !== 'debugger') return;
if (mode.type !== 'debugger') return;

let ws: WebSocket | null = null;
let reconnectTimeout: NodeJS.Timeout | null = null;
let hasConnectedOnce = false;

const connect = () => {
try {
ws = new WebSocket(WS_URL);

ws.onopen = () => {
setConnected(true);
if (!hasConnectedOnce) {
console.log('Connected to debugger');
console.log('Connected to debugger');
hasConnectedOnce = true;
}
};

ws.onclose = () => {
setConnected(false);
// Only log disconnect once
if (hasConnectedOnce) {
console.log('⚠️ Disconnected from debugger');
console.log('Disconnected from debugger');
hasConnectedOnce = false;
}
reconnectTimeout = setTimeout(connect, 1000);
};

// Suppress error logging - onclose already handles disconnections

ws.onerror = () => {
setConnected(false);
// Errors are handled by onclose, no need to log here
};

ws.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
setContent(data.content || '');
} catch (error) {
// Only log parse errors, not connection errors
console.error('Failed to parse message:', error);
}
};
Expand All @@ -66,18 +66,33 @@ export default function App() {
reconnectTimeout = setTimeout(connect, 1000);
}
};

connect();

return () => {
if (reconnectTimeout) clearTimeout(reconnectTimeout);
ws?.close();
};
}, [mode]);
}, [mode.type]);

// Chat History Test Mode
if (mode === 'chat-history') {
return <ChatHistoryTest onBack={() => setMode('debugger')} />;
// List Picker Screen
if (mode.type === 'list-picker') {
return (
<ListPickerScreen
onSelect={(listType) => setMode({ type: 'chat-test', listType })}
onBack={() => setMode({ type: 'debugger' })}
/>
);
}

// Chat Test Screen
if (mode.type === 'chat-test') {
return (
<ChatTestScreen
listType={mode.listType}
onBack={() => setMode({ type: 'list-picker' })}
/>
);
}

// Default: Debugger Mode
Expand All @@ -88,15 +103,15 @@ export default function App() {
<Text style={styles.statusText}>
{connected ? 'Connected to debugger' : 'Waiting for debugger...'}
</Text>
<Pressable
onPress={() => setMode('chat-history')}
<Pressable
onPress={() => setMode({ type: 'list-picker' })}
style={styles.modeButton}
>
<Text style={styles.modeButtonText}>💬 Chat Test</Text>
<Text style={styles.modeButtonText}>Test Chats</Text>
</Pressable>
</View>
<ScrollView
style={styles.content}
<ScrollView
style={styles.content}
contentContainerStyle={styles.contentContainer}
>
{content ? (
Expand All @@ -109,7 +124,7 @@ export default function App() {
Waiting for content from web debugger...
</Text>
<Text style={styles.hint}>
Or tap "💬 Chat Test" above to test chat history rendering
Or tap "Test Chats" to test list rendering
</Text>
</View>
)}
Expand Down Expand Up @@ -147,15 +162,15 @@ const styles = StyleSheet.create((theme) => ({
fontSize: 14,
},
modeButton: {
backgroundColor: '#2a2a2a',
paddingHorizontal: 12,
paddingVertical: 6,
backgroundColor: '#4ade80',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 16,
},
modeButtonText: {
color: '#fff',
fontSize: 12,
fontWeight: '500',
color: '#000',
fontSize: 14,
fontWeight: '600',
},
content: {
flex: 1,
Expand Down
Loading