feat: add multimedia rendering in dispute chat (Phase 4)#514
Conversation
- Decouple EncryptedImageMessage and EncryptedFileMessage from chatRoomsProvider, accepting generic callbacks instead - Change multimedia widgets from ConsumerStatefulWidget to StatefulWidget (no longer need Riverpod) - Update MessageBubble (P2P) to pass chatRoomsProvider callbacks to decoupled widgets - Add media cache to DisputeChatNotifier (image/file cache maps + getter/setter methods) - Add _processMessageContent to DisputeChatNotifier for auto-downloading images on receive - Replace placeholder rendering in DisputeMessageBubble with real EncryptedImageMessage/EncryptedFileMessage widgets - Change DisputeMessageBubble from StatelessWidget to ConsumerWidget, add disputeId parameter - Pass disputeId from DisputeMessagesList to DisputeMessageBubble - Both P2P and dispute chat now share the same multimedia rendering widgets - Zero regression on existing P2P chat functionality
WalkthroughReplaces Riverpod provider access in encrypted media widgets with explicit callback props; message bubbles now supply notifier-backed callbacks; dispute chat notifier adds decrypted-media caches, metadata stores, and background processing for encrypted image/file messages. Changes
Sequence Diagram(s)sequenceDiagram
participant Notifier as DisputeChatNotifier
participant Service as EncryptedUploadService
participant Cache as LocalCache
participant Widget as EncryptedMediaWidget
Note over Notifier,Service: Unwrapping / historical processing of events
Notifier->>Notifier: _processMessageContent(event)
alt encrypted image/file detected
Notifier->>Service: downloadAndDecrypt(payload, sharedKey)
Service-->>Notifier: decryptedData + metadata
Notifier->>Cache: cacheDecrypted*(messageId, decryptedData, metadata)
end
Note over Widget,Notifier: Widget runtime access (render / user open)
Widget->>Notifier: getCached*(messageId)
Notifier-->>Widget: cachedData?
Widget->>Notifier: get*Metadata(messageId)
Notifier-->>Widget: metadata
alt cachedData missing
Widget->>Notifier: getSharedKey()
Widget->>Service: requestDecrypt(payload, sharedKey)
Service-->>Notifier: decryptedData + metadata
Notifier->>Cache: cacheDecrypted*(messageId, decryptedData, metadata)
Notifier-->>Widget: decryptedData
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
lib/features/chat/widgets/encrypted_file_message.dart (2)
119-125:⚠️ Potential issue | 🟡 MinorHardcoded user-facing string should be localized.
The string
'Failed to load image'on line 120 should useS.of(context)!.failedToLoadImageor a similar localization key for consistency with the coding guidelines.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/chat/widgets/encrypted_file_message.dart` around lines 119 - 125, Replace the hardcoded user-facing string in the Text widget inside encrypted_file_message.dart ("Failed to load image") with the localized resource access (e.g. S.of(context)!.failedToLoadImage) so the message is localized; keep the existing TextStyle usage but pass the localized string into the Text constructor in the same spot where the literal currently appears (refer to the Text widget block that sets color: AppTheme.textSecondary and fontSize: 12).
371-378:⚠️ Potential issue | 🟡 MinorHardcoded user-facing string should be localized.
The string
'Failed to load file'on line 372 should use a localization key (e.g.,S.of(context)!.failedToLoadFile) for consistency. As per coding guidelines: "Localize all user-facing strings via ARB files and access them withS.of(context)rather than hard-coded string literals."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/chat/widgets/encrypted_file_message.dart` around lines 371 - 378, Replace the hardcoded Text('Failed to load file') in the widget (in encrypted_file_message.dart, inside the build method/Widget that renders the error Text) with the localized string S.of(context)!.failedToLoadFile; ensure the file imports the generated localization (import 'package:your_app/generated/l10n.dart'; or equivalent) and that the ARB contains the failedToLoadFile key so S.of(context) exposes it; keep the existing TextStyle and TextAlign unchanged.
🧹 Nitpick comments (4)
lib/features/disputes/notifiers/dispute_chat_notifier.dart (3)
253-291: Historical messages don't pre-process encrypted media.When loading historical messages in
_loadHistoricalMessages, the_processMessageContentmethod is not called on unwrapped events. This means encrypted images/files won't be auto-downloaded and cached when the user opens an existing dispute chat, unlike new incoming messages in_onChatEvent.Consider adding media processing after unwrapping historical messages for consistency:
// Decrypt and unwrap the message final unwrappedEvent = await storedEvent.p2pUnwrap(session.adminSharedKey!); + await _processMessageContent(unwrappedEvent); messages.add(DisputeChatMessage(event: unwrappedEvent));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/disputes/notifiers/dispute_chat_notifier.dart` around lines 253 - 291, The historical loader is unwrapping events but not pre-processing encrypted media, so after reconstructing and unwrapping each storedEvent in the loop (the variable unwrappedEvent returned by storedEvent.p2pUnwrap in the method that loads historical messages) call the existing media processing routine used for incoming events (invoke _processMessageContent on the unwrappedEvent and await it if async) before creating/adding the DisputeChatMessage; this ensures encrypted images/files are downloaded/cached the same way as in _onChatEvent and keeps media handling consistent.
478-491: Service instantiation on every call.
EncryptedImageUploadService()is instantiated per image processed. If the service is stateless, consider creating it once as a field or obtaining it via a provider for consistency with the codebase's dependency injection patterns.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/disputes/notifiers/dispute_chat_notifier.dart` around lines 478 - 491, The _processEncryptedImageMessage is creating a new EncryptedImageUploadService() for every call; instead, make EncryptedImageUploadService a single instance (either a private final field on the notifier class or injected via the class constructor/provider) and use that instance inside _processEncryptedImageMessage (keep using getAdminSharedKey(), EncryptedImageUploadResult.fromJson, downloadAndDecryptImage and cacheDecryptedImage as before) to avoid repeated instantiation and align with DI patterns.
424-448: In-memory caches grow unbounded.The
_imageCacheand_fileCachemaps store decrypted media indefinitely. In a long dispute conversation with many media attachments, this could lead to memory pressure. Consider adding an eviction strategy (LRU cache, max entry count, or clearing on dispose).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/disputes/notifiers/dispute_chat_notifier.dart` around lines 424 - 448, The in-memory caches _imageCache and _fileCache (and their metadata maps _imageMetadata/_fileMetadata) grow unbounded; implement an eviction strategy by capping entries and evicting least-recently-used items (or oldest) inside cacheDecryptedImage and cacheDecryptedFile, or replace the plain Map with an LRU cache implementation (or a package) and clear caches in a dispose/close method; update getCachedImage/getCachedFile to refresh recency on access and ensure cache size/config (max entries) is configurable.lib/features/chat/widgets/encrypted_file_message.dart (1)
526-544: Inconsistent diacritic handling compared to EncryptedImageMessage.This file manually replaces diacritics (lines 533-537) while
encrypted_image_message.dartuses thediacriticpackage'sremoveDiacritics()function. Consider using the same approach for consistency and more comprehensive Unicode support.♻️ Suggested refactor
+import 'package:diacritic/diacritic.dart'; ... String _sanitizeFilename(String filename) { // 1. Get basename only (remove any directory components) final basename = filename.split(RegExp(r'[/\\]')).last; - // 2. Normalize accented characters to prevent encoding issues - String normalized = basename - .replaceAll('á', 'a').replaceAll('é', 'e').replaceAll('í', 'i') - .replaceAll('ó', 'o').replaceAll('ú', 'u').replaceAll('ñ', 'n') - .replaceAll('ü', 'u').replaceAll('Á', 'A').replaceAll('É', 'E') - .replaceAll('Í', 'I').replaceAll('Ó', 'O').replaceAll('Ú', 'U') - .replaceAll('Ñ', 'N').replaceAll('Ü', 'U'); + // 2. Normalize using diacritic package for comprehensive Unicode support + String normalized = removeDiacritics(basename);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/chat/widgets/encrypted_file_message.dart` around lines 526 - 544, The _sanitizeFilename function currently does manual diacritic replacements; replace that manual block with the diacritic package's removeDiacritics() call for consistent and comprehensive normalization (apply removeDiacritics to the basename before replacing spaces/unsafe chars), remove the chain of replaceAll(...) for accented chars, add the diacritic import (import 'package:diacritic/diacritic.dart';) and ensure the package is in pubspec.yaml if not already; keep the remaining steps (basename extraction, whitespace->underscore, dangerous-char regex, '..' replacement) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@lib/features/chat/widgets/encrypted_file_message.dart`:
- Around line 119-125: Replace the hardcoded user-facing string in the Text
widget inside encrypted_file_message.dart ("Failed to load image") with the
localized resource access (e.g. S.of(context)!.failedToLoadImage) so the message
is localized; keep the existing TextStyle usage but pass the localized string
into the Text constructor in the same spot where the literal currently appears
(refer to the Text widget block that sets color: AppTheme.textSecondary and
fontSize: 12).
- Around line 371-378: Replace the hardcoded Text('Failed to load file') in the
widget (in encrypted_file_message.dart, inside the build method/Widget that
renders the error Text) with the localized string
S.of(context)!.failedToLoadFile; ensure the file imports the generated
localization (import 'package:your_app/generated/l10n.dart'; or equivalent) and
that the ARB contains the failedToLoadFile key so S.of(context) exposes it; keep
the existing TextStyle and TextAlign unchanged.
---
Nitpick comments:
In `@lib/features/chat/widgets/encrypted_file_message.dart`:
- Around line 526-544: The _sanitizeFilename function currently does manual
diacritic replacements; replace that manual block with the diacritic package's
removeDiacritics() call for consistent and comprehensive normalization (apply
removeDiacritics to the basename before replacing spaces/unsafe chars), remove
the chain of replaceAll(...) for accented chars, add the diacritic import
(import 'package:diacritic/diacritic.dart';) and ensure the package is in
pubspec.yaml if not already; keep the remaining steps (basename extraction,
whitespace->underscore, dangerous-char regex, '..' replacement) unchanged.
In `@lib/features/disputes/notifiers/dispute_chat_notifier.dart`:
- Around line 253-291: The historical loader is unwrapping events but not
pre-processing encrypted media, so after reconstructing and unwrapping each
storedEvent in the loop (the variable unwrappedEvent returned by
storedEvent.p2pUnwrap in the method that loads historical messages) call the
existing media processing routine used for incoming events (invoke
_processMessageContent on the unwrappedEvent and await it if async) before
creating/adding the DisputeChatMessage; this ensures encrypted images/files are
downloaded/cached the same way as in _onChatEvent and keeps media handling
consistent.
- Around line 478-491: The _processEncryptedImageMessage is creating a new
EncryptedImageUploadService() for every call; instead, make
EncryptedImageUploadService a single instance (either a private final field on
the notifier class or injected via the class constructor/provider) and use that
instance inside _processEncryptedImageMessage (keep using getAdminSharedKey(),
EncryptedImageUploadResult.fromJson, downloadAndDecryptImage and
cacheDecryptedImage as before) to avoid repeated instantiation and align with DI
patterns.
- Around line 424-448: The in-memory caches _imageCache and _fileCache (and
their metadata maps _imageMetadata/_fileMetadata) grow unbounded; implement an
eviction strategy by capping entries and evicting least-recently-used items (or
oldest) inside cacheDecryptedImage and cacheDecryptedFile, or replace the plain
Map with an LRU cache implementation (or a package) and clear caches in a
dispose/close method; update getCachedImage/getCachedFile to refresh recency on
access and ensure cache size/config (max entries) is configurable.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2cef150b-d966-4721-914a-bc42f9fdf69d
📒 Files selected for processing (6)
lib/features/chat/widgets/encrypted_file_message.dartlib/features/chat/widgets/encrypted_image_message.dartlib/features/chat/widgets/message_bubble.dartlib/features/disputes/notifiers/dispute_chat_notifier.dartlib/features/disputes/widgets/dispute_message_bubble.dartlib/features/disputes/widgets/dispute_messages_list.dart
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/features/chat/widgets/encrypted_file_message.dart (1)
119-126:⚠️ Potential issue | 🟡 MinorHardcoded user-facing strings should be localized.
Lines 121 and 373 contain hardcoded English text that should use
S.of(context)!for localization:
- Line 121:
'Failed to load image'- Line 373:
'Failed to load file'🌐 Proposed fix for localization
- Text( - 'Failed to load image', - style: TextStyle( + Text( + S.of(context)!.failedToLoadImage, + style: TextStyle(- Text( - 'Failed to load file', - style: TextStyle( + Text( + S.of(context)!.failedToLoadFile, + style: TextStyle(Add the corresponding keys to the ARB files.
As per coding guidelines: "Replace all hardcoded user-facing text with localized strings using
S.of(context)!.keyName."Also applies to: 372-378
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/chat/widgets/encrypted_file_message.dart` around lines 119 - 126, Replace the hardcoded user-facing strings in EncryptedFileMessage widget: change the Text('Failed to load image') and Text('Failed to load file') usages to use localized resources via S.of(context)!, e.g. S.of(context)!.failedToLoadImage and S.of(context)!.failedToLoadFile; update the ARB files with those new keys and run the localization codegen so the S class contains the new accessors.
🧹 Nitpick comments (1)
lib/features/disputes/notifiers/dispute_chat_notifier.dart (1)
425-431: Unbounded in-memory caches may cause memory pressure in long sessions.The four cache maps (
_imageCache,_imageMetadata,_fileCache,_fileMetadata) grow without bound as media messages are processed, anddispose()does not clear them. For disputes with many media messages or long-running sessions, this could lead to significant memory consumption.Consider clearing the caches in
dispose():♻️ Proposed fix to clear caches on dispose
`@override` void dispose() { _subscription?.cancel(); _sessionListener?.close(); + _imageCache.clear(); + _imageMetadata.clear(); + _fileCache.clear(); + _fileMetadata.clear(); super.dispose(); }Also applies to: 551-556
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/disputes/notifiers/dispute_chat_notifier.dart` around lines 425 - 431, The maps _imageCache, _imageMetadata, _fileCache, and _fileMetadata are unbounded and not cleared, causing memory pressure; update the DisputeChatNotifier.dispose() method to clear (call .clear() on) each of those maps (and any other similar caches around lines referenced like the other cache pair at 551-556) to free memory on teardown, and optionally replace or wrap these maps with a bounded/LRU cache implementation if long-lived sessions need retention rather than full clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@lib/features/chat/widgets/encrypted_file_message.dart`:
- Around line 119-126: Replace the hardcoded user-facing strings in
EncryptedFileMessage widget: change the Text('Failed to load image') and
Text('Failed to load file') usages to use localized resources via
S.of(context)!, e.g. S.of(context)!.failedToLoadImage and
S.of(context)!.failedToLoadFile; update the ARB files with those new keys and
run the localization codegen so the S class contains the new accessors.
---
Nitpick comments:
In `@lib/features/disputes/notifiers/dispute_chat_notifier.dart`:
- Around line 425-431: The maps _imageCache, _imageMetadata, _fileCache, and
_fileMetadata are unbounded and not cleared, causing memory pressure; update the
DisputeChatNotifier.dispose() method to clear (call .clear() on) each of those
maps (and any other similar caches around lines referenced like the other cache
pair at 551-556) to free memory on teardown, and optionally replace or wrap
these maps with a bounded/LRU cache implementation if long-lived sessions need
retention rather than full clear.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 70ed68f1-3c0d-4592-a8c8-16f2a1e2904b
📒 Files selected for processing (2)
lib/features/chat/widgets/encrypted_file_message.dartlib/features/disputes/notifiers/dispute_chat_notifier.dart
There was a problem hiding this comment.
Review — Phase 4: Multimedia in Dispute Chat
✅ What works well
- Decoupling pattern is solid. Moving
EncryptedImageMessageandEncryptedFileMessagefromConsumerStatefulWidget→StatefulWidgetwith callback injection is the right call. Makes both widgets reusable across P2P and dispute contexts without being coupled to a specific provider. - CI passes, no merge conflicts,
mergeable_state: clean. diacriticpackage adoption inencrypted_file_message.dart— the old manualreplaceAllchain was incomplete (missed characters likeä,ö,ç, etc.). Good fix.- Historical messages now call
_processMessageContent(line 284) — this was missing in an earlier commit and CodeRabbit flagged it. Fixed correctly. - Auto-download strategy is sensible: images auto-download, non-image files cache metadata only and require manual trigger.
🚨 Issues (must fix)
1. _processMessageContent blocks message rendering (UX regression)
In _onChatEvent (~line 200), await _processMessageContent(unwrappedEvent) is called before the message is added to state. This means:
- If the image download takes 5-10 seconds (or times out), the message won't appear in the chat until the download completes or fails.
- For the P2P chat, images load lazily (widget auto-loads on build). The dispute chat should follow the same pattern.
Fix: Don't await the processing. Fire-and-forget with unawaited() or move it after adding the message to state, then trigger a state update when the cache is populated:
messages.add(DisputeChatMessage(event: unwrappedEvent));
// Fire-and-forget — widget will pick up cached data on next build
unawaited(_processMessageContent(unwrappedEvent));Same applies to the historical messages loop at line 284.
2. Unbounded in-memory caches — memory leak
The four maps (_imageCache, _imageMetadata, _fileCache, _fileMetadata) grow without bounds and are never cleared, not even in dispose().
A dispute with 20 high-res images could consume 100+ MB of RAM. On low-end Android devices this will cause OOM kills.
Minimum fix: Clear caches in dispose():
@override
void dispose() {
_subscription?.cancel();
_sessionListener?.close();
_imageCache.clear();
_imageMetadata.clear();
_fileCache.clear();
_fileMetadata.clear();
super.dispose();
}Better fix: Cap cache size (e.g., 50 entries, evict oldest).
⚠️ Minor issues
3. Hardcoded strings not localized (pre-existing, but touched by this PR)
encrypted_file_message.dart lines 121 and 373 have 'Failed to load image' and 'Failed to load file' hardcoded. These should use S.of(context)! as per project guidelines. Since this PR touches these files and adds the diacritic import, it's a good time to fix them.
4. Service instantiation per call
_processEncryptedImageMessage and _processEncryptedFileMessage create new instances of EncryptedImageUploadService() and EncryptedFileUploadService() on every call. If these are stateless, make them late final fields on the notifier.
5. DisputeMessageBubble re-couples to provider
The multimedia widgets were decoupled from providers (good), but DisputeMessageBubble was changed from StatelessWidget → ConsumerWidget to read the dispute notifier directly. This is the pragmatic choice for now, but ideally the callbacks would be passed down from DisputeMessagesList to keep the bubble provider-free too. Not a blocker, just noting the architecture direction.
Summary
The refactoring direction is correct and the code sharing between P2P/dispute chat is a real improvement. But issue #1 (blocking message rendering) is a UX regression that needs fixing before merge, and issue #2 (memory leak) is a must-fix that's trivial to address.
Request changes for #1 and #2. The rest are nice-to-haves for this PR.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lib/features/disputes/notifiers/dispute_chat_notifier.dart (1)
10-10: Avoid decoding the same payload twice.
_processMessageContentalready hasjsonContent, but the helpers inlib/features/chat/utils/message_type_helpers.dart:6-30decodemessage.contentagain. Switching on the parsedtypehere removes the extra decode on every media message/history replay and lets this notifier drop the helper import.♻️ Example simplification
-import 'package:mostro_mobile/features/chat/utils/message_type_helpers.dart'; ... - if (jsonContent != null) { - if (MessageTypeUtils.isEncryptedImageMessage(message)) { - await _processEncryptedImageMessage(message, jsonContent); - } else if (MessageTypeUtils.isEncryptedFileMessage(message)) { - await _processEncryptedFileMessage(message, jsonContent); - } - } + switch (jsonContent?['type']) { + case 'image_encrypted': + await _processEncryptedImageMessage(message, jsonContent!); + break; + case 'file_encrypted': + await _processEncryptedFileMessage(message, jsonContent!); + break; + }Also applies to: 458-473
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/features/disputes/notifiers/dispute_chat_notifier.dart` at line 10, The notifier decodes message.content twice: _processMessageContent already parses jsonContent but code and the helpers in message_type_helpers.dart re-decode message.content; update the logic inside _processMessageContent (and the similar block at the other occurrence) to switch on the already-parsed jsonContent['type'] (or its parsed enum) and call the appropriate handling directly instead of calling the helpers that re-decode message.content, then remove the now-unused import of package:mostro_mobile/features/chat/utils/message_type_helpers.dart; ensure you update both the main occurrence and the other block referenced (lines ~458-473) to avoid duplicate decoding.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@lib/features/disputes/notifiers/dispute_chat_notifier.dart`:
- Line 10: The notifier decodes message.content twice: _processMessageContent
already parses jsonContent but code and the helpers in message_type_helpers.dart
re-decode message.content; update the logic inside _processMessageContent (and
the similar block at the other occurrence) to switch on the already-parsed
jsonContent['type'] (or its parsed enum) and call the appropriate handling
directly instead of calling the helpers that re-decode message.content, then
remove the now-unused import of
package:mostro_mobile/features/chat/utils/message_type_helpers.dart; ensure you
update both the main occurrence and the other block referenced (lines ~458-473)
to avoid duplicate decoding.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bcd614ea-9dfd-4798-9729-8b3ecdac6511
📒 Files selected for processing (1)
lib/features/disputes/notifiers/dispute_chat_notifier.dart
There was a problem hiding this comment.
Both critical issues from my previous review are properly addressed in 497cce80:
✅ Issue #1 (blocking rendering): _processMessageContent is now fire-and-forget with unawaited() — both for incoming messages (after adding to state) and historical messages. Messages appear immediately, media loads in background.
✅ Issue #2 (memory leak): All four cache maps are cleared in dispose().
CI passes, no conflicts. LGTM 👍
How to test
Dispute chat multimedia (new)
P2P chat regression (must still work)
Summary by CodeRabbit
New Features
Refactor