feat(chats): export conversation as a plain-text transcript#531
Conversation
Adds a "share" swipe action next to delete on the Chats list, letting users get a conversation out of the app as readable text via the native share sheet. There was previously no way to extract chat history from a release build without root/adb access. - src/utils/exportConversation.ts: pure formatConversationAsText() transcript serializer + shareConversationAsText() wrapper. Short transcripts go through RN's core Share sheet; long transcripts are written to a .txt file and handed to react-native-share, since core Share silently drops file attachments on Android. - ChatsListScreen: export swipe action (share-2 icon) alongside the existing delete action. - 100% unit coverage on the new formatter/share module, plus integration tests on ChatsListScreen for the wiring. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing |
📝 WalkthroughWalkthroughChangesConversation export
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant ChatList
participant ExportUtility
participant InlineShare
participant FileSystem
participant NativeShare
ChatList->>ExportUtility: shareConversationAsText(conversation)
ExportUtility->>ExportUtility: formatConversationAsText(conversation)
alt Short transcript
ExportUtility->>InlineShare: Share.share(text)
else Long transcript
ExportUtility->>FileSystem: writeFile(cache path, text)
ExportUtility->>NativeShare: open(file share options)
end
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
package.jsonParsing error: Unexpected token : Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
| import { Conversation, Message } from '../types'; | ||
|
|
||
| /** Above this many lines, the transcript is shared as a .txt file instead of inline text. */ | ||
| const FILE_SHARE_LINE_THRESHOLD = 30; |
There was a problem hiding this comment.
⚠️ Edge Case: File-share threshold counts lines, not size — Android truncation
shareConversationAsText decides between the file path and the inline Share.share path using transcript.split(' ').length > FILE_SHARE_LINE_THRESHOLD (30 lines). The stated purpose of the file path is that RN's core Share silently drops/truncates large payloads on Android. But line count is a poor proxy for payload size: a conversation with a single very long assistant response (common with LLM output) serializes to only ~6 lines — one timestamp line plus one content line per message — so it stays under the threshold and routes through the inline Share.share. On Android, that large message is delivered as an intent extra, which is size-limited (~hundreds of KB via the Binder transaction buffer) and can be truncated or fail. The result is a platform-specific bug: the exact case the file path was designed to handle (large transcripts) can still be routed through the broken inline path. Consider thresholding on transcript.length (character count) instead of, or in addition to, line count.
Route by serialized character length so large single-message chats also use the file path.:
const FILE_SHARE_CHAR_THRESHOLD = 8000; // ~intent-extra safe limit
...
if (transcript.length > FILE_SHARE_CHAR_THRESHOLD) {
const filePath = `${RNFS.CachesDirectoryPath}/${sanitizeFileName(conversation.title)}.txt`;
await RNFS.writeFile(filePath, transcript, 'utf8');
await RNShare.open({ /* ...unchanged... */ });
return;
}
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| } catch { | ||
| // User cancelled or the share sheet failed to open — nothing to recover. | ||
| } |
There was a problem hiding this comment.
💡 Quality: Empty catch swallows genuine share/write failures silently
The catch {} in shareConversationAsText discards every error, with a comment assuming it is only user-cancellation. But RNFS.writeFile (disk full, permission denied) and RNShare.open/Share.share (no share targets, native failure) throw here too. In those cases the user taps the share action and nothing happens — no toast, no alert, no log — which is indistinguishable from a bug and gives no feedback. react-native-share also rejects on user cancel, so cancellation and real failures can't be told apart from an empty catch. Consider distinguishing cancellation from real errors and surfacing genuine failures via the app's alert/toast mechanism (and at minimum logging them), consistent with the project guideline that side effects report their outcome to the UI.
Ignore cancellation but log/surface real failures.:
} catch (err: any) {
// react-native-share rejects with 'User did not share' on cancel.
if (err?.message?.includes('User did not share')) return;
console.warn('[exportConversation] share failed', err);
// surface a user-facing toast/alert here
}
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
Code Review
|
| Auto-apply | Compact |
|
|
Was this helpful? React with 👍 / 👎 | Gitar
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/utils/exportConversation.ts (1)
62-84: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winTemp file is never cleaned up after sharing.
RNFS.writeFilecreates a.txtfile in the cache directory, but it's never deleted afterRNShare.openresolves. Repeated sharing of long conversations accumulates orphaned files until the OS reclaims cache space.Add cleanup after the share sheet dismisses. Note: on Android, the target app may still be reading the content URI when
RNShare.openresolves, so test cleanup timing on device.♻️ Proposed cleanup after share
await RNShare.open({ url: `file://${filePath}`, type: 'text/plain', filename: sanitizeFileName(conversation.title), title: conversation.title, failOnCancel: false, }); + // Clean up the temp file after the share sheet is dismissed. + // On Android the target app may still read the content URI briefly, + // so a short delay avoids deleting the file mid-read. + setTimeout(() => RNFS.unlink(filePath).catch(() => {}), 3000); return;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/utils/exportConversation.ts` around lines 62 - 84, Update shareConversationAsText around RNFS.writeFile and RNShare.open to delete the generated filePath after the share sheet completes, including when sharing fails or is cancelled. Ensure cleanup occurs after RNShare.open resolves while preserving the existing short-transcript sharing path and error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/screens/ChatsListScreen.tsx`:
- Around line 165-182: Add an accessibilityLabel to the export TouchableOpacity
in renderRightActions, clearly identifying it as the action for exporting or
sharing the conversation. Leave the existing behavior and delete button
unchanged.
---
Nitpick comments:
In `@src/utils/exportConversation.ts`:
- Around line 62-84: Update shareConversationAsText around RNFS.writeFile and
RNShare.open to delete the generated filePath after the share sheet completes,
including when sharing fails or is cancelled. Ensure cleanup occurs after
RNShare.open resolves while preserving the existing short-transcript sharing
path and error handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 66fc047f-1905-41cf-ac71-7155f32c9097
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
__tests__/rntl/screens/ChatsListScreen.test.tsx__tests__/unit/utils/exportConversation.test.tsjest.config.jsjest.setup.tspackage.jsonsrc/screens/ChatsListScreen.tsxsrc/utils/exportConversation.ts
| const renderRightActions = (conversation: Conversation) => ( | ||
| <TouchableOpacity | ||
| style={styles.deleteAction} | ||
| onPress={() => handleDeleteChat(conversation)} | ||
| > | ||
| <Icon name="trash-2" size={16} color={colors.error} /> | ||
| </TouchableOpacity> | ||
| <View style={styles.rightActions}> | ||
| <TouchableOpacity | ||
| style={styles.exportAction} | ||
| onPress={() => shareConversationAsText(conversation)} | ||
| testID="export-conversation-button" | ||
| > | ||
| <Icon name="share-2" size={16} color={colors.primary} /> | ||
| </TouchableOpacity> | ||
| <TouchableOpacity | ||
| style={styles.deleteAction} | ||
| onPress={() => handleDeleteChat(conversation)} | ||
| testID="delete-conversation-button" | ||
| > | ||
| <Icon name="trash-2" size={16} color={colors.error} /> | ||
| </TouchableOpacity> | ||
| </View> | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add accessibilityLabel to the export button.
The export TouchableOpacity is icon-only with no accessibilityLabel. Screen reader users can't identify the action. The existing delete button has the same gap, but new code should set the label.
♿ Proposed accessibility fix
<TouchableOpacity
style={styles.exportAction}
onPress={() => shareConversationAsText(conversation)}
testID="export-conversation-button"
+ accessibilityLabel="Export conversation"
+ accessibilityRole="button"
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const renderRightActions = (conversation: Conversation) => ( | |
| <TouchableOpacity | |
| style={styles.deleteAction} | |
| onPress={() => handleDeleteChat(conversation)} | |
| > | |
| <Icon name="trash-2" size={16} color={colors.error} /> | |
| </TouchableOpacity> | |
| <View style={styles.rightActions}> | |
| <TouchableOpacity | |
| style={styles.exportAction} | |
| onPress={() => shareConversationAsText(conversation)} | |
| testID="export-conversation-button" | |
| > | |
| <Icon name="share-2" size={16} color={colors.primary} /> | |
| </TouchableOpacity> | |
| <TouchableOpacity | |
| style={styles.deleteAction} | |
| onPress={() => handleDeleteChat(conversation)} | |
| testID="delete-conversation-button" | |
| > | |
| <Icon name="trash-2" size={16} color={colors.error} /> | |
| </TouchableOpacity> | |
| </View> | |
| ); | |
| const renderRightActions = (conversation: Conversation) => ( | |
| <View style={styles.rightActions}> | |
| <TouchableOpacity | |
| style={styles.exportAction} | |
| onPress={() => shareConversationAsText(conversation)} | |
| testID="export-conversation-button" | |
| accessibilityLabel="Export conversation" | |
| accessibilityRole="button" | |
| > | |
| <Icon name="share-2" size={16} color={colors.primary} /> | |
| </TouchableOpacity> | |
| <TouchableOpacity | |
| style={styles.deleteAction} | |
| onPress={() => handleDeleteChat(conversation)} | |
| testID="delete-conversation-button" | |
| > | |
| <Icon name="trash-2" size={16} color={colors.error} /> | |
| </TouchableOpacity> | |
| </View> | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/screens/ChatsListScreen.tsx` around lines 165 - 182, Add an
accessibilityLabel to the export TouchableOpacity in renderRightActions, clearly
identifying it as the action for exporting or sharing the conversation. Leave
the existing behavior and delete button unchanged.



Adds a "share" swipe action next to delete on the Chats list, letting users get a conversation out of the app as readable text via the native share sheet.
Summary
Type of Change
Screenshots / Screen Recordings
Android
iOS
Checklist
General
Testing
npm test)React Native Specific
project.pbxproj)SPACING/TYPOGRAPHYconstants from the themeuseThemedStylespattern (not inline or staticStyleSheet.create)FlatList/FlashList(not.map()insideScrollView)Performance & Models
/vs\\)Security
Related Issues
Additional Notes
Summary by CodeRabbit
New Features
Bug Fixes