From 78a3fbffb031b3b423c2b898bbb64444b1005dea Mon Sep 17 00:00:00 2001 From: "japan.shah" Date: Fri, 30 Jan 2026 13:31:19 +0530 Subject: [PATCH] fix: :bug: ensure images are properly added to preview when sending messages with text --- CHANGELOG.md | 3 + doc/documentation.md | 159 +++++++------ example/lib/main.dart | 43 +++- example/lib/widgets/image_preview_screen.dart | 212 ++++++++++++++++++ .../send_message_configuration.dart | 4 - lib/src/values/typedefs.dart | 2 +- lib/src/widgets/chatui_textfield.dart | 2 - lib/src/widgets/send_message_widget.dart | 33 +-- 8 files changed, 350 insertions(+), 108 deletions(-) create mode 100644 example/lib/widgets/image_preview_screen.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e22beb2..8b0e008f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Rendering issue in attached image preview when sending message on web. * **Feat**: [420](https://github.com/SimformSolutionsPvtLtd/chatview/pull/420) Added support for `playerMode` in `VoiceMessageConfiguration` with `single` and `multi`. +* **Breaking**: [430](https://github.com/SimformSolutionsPvtLtd/chatview/pull/430) Removed + `shouldSendImageWithText` parameter. The example app now demonstrates an image preview screen with optional text + captions using `GalleryActionButton` and a custom preview handler to achieve similar functionality. ## [3.0.0] diff --git a/doc/documentation.md b/doc/documentation.md index 29f73003..062e1a74 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -6,7 +6,7 @@ Flutter applications with [Flexible Backend Integration](https://pub.dev/package ## Preview | ChatList | ChatView | -|------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------| +| ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | ![ChatList_Preview](https://raw.githubusercontent.com/SimformSolutionsPvtLtd/chatview/main/preview/chatlist.gif) | ![ChatView Preview](https://raw.githubusercontent.com/SimformSolutionsPvtLtd/chatview/main/preview/chatview.gif) | ## Features @@ -46,20 +46,19 @@ For a live web demo, visit [Chat View Example](https://simformsolutionspvtltd.gi ## Compatibility with [chatview_connect](https://pub.dev/packages/chatview_connect) | chatview version | [chatview_connect](https://pub.dev/packages/chatview_connect) version | -|------------------|-----------------------------------------------------------------------| +| ---------------- | --------------------------------------------------------------------- | | `>=2.4.1 <3.0.0` | `0.0.1` | | `>= 3.0.0` | `3.0.0` | ## Compatible Message Types | Message Types | Android | iOS | MacOS | Web | Linux | Windows | -|:---------------:|:-------:|:---:|:-----:|:---:|:-----:|:-------:| +| :-------------: | :-----: | :-: | :---: | :-: | :---: | :-----: | | Text messages | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | Image messages | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | -| Voice messages | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | +| Voice messages | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | | Custom messages | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | - # Installation ## Adding the dependency @@ -84,6 +83,7 @@ import 'package:chatview/chatview.dart'; ### For Image Picker #### iOS + Add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: ```xml @@ -98,24 +98,30 @@ Add the following keys to your _Info.plist_ file, located in `/ios ### For Voice Messages #### iOS -* Add this row in `ios/Runner/Info.plist`: + +- Add this row in `ios/Runner/Info.plist`: + ```xml NSMicrophoneUsageDescription This app requires Mic permission. ``` -* This plugin requires iOS 13.0 or higher. Add this line in `Podfile`: +- This plugin requires iOS 13.0 or higher. Add this line in `Podfile`: + ```ruby platform :ios, '13.0' ``` #### Android -* Change the minimum Android SDK version to 21 (or higher) in your `android/app/build.gradle` file: + +- Change the minimum Android SDK version to 21 (or higher) in your `android/app/build.gradle` file: + ```gradle minSdkVersion 21 ``` -* Add RECORD_AUDIO permission in `AndroidManifest.xml`: +- Add RECORD_AUDIO permission in `AndroidManifest.xml`: + ```xml ``` @@ -499,7 +505,7 @@ ChatList( ) ``` -## ChatList States Configuration +## ChatList States Configuration ```dart ChatList( @@ -597,7 +603,7 @@ void onSendTap(String message, ReplyMessage replyMessage, MessageType messageTyp replyMessage: replyMessage, messageType: messageType, ); - + // Add message to chat controller chatController.addMessage(newMessage); } @@ -677,14 +683,14 @@ ChatView( ### Loading Old Reply Messages -The `loadOldReplyMessage` callback is essential for handling replies to messages that aren't -currently loaded in the chat view. When a user taps on a replied message, ChatView automatically -searches for the original message in the current message list. If the original message isn't -found (typically because it's an older message), this callback is triggered to load the +The `loadOldReplyMessage` callback is essential for handling replies to messages that aren't +currently loaded in the chat view. When a user taps on a replied message, ChatView automatically +searches for the original message in the current message list. If the original message isn't +found (typically because it's an older message), this callback is triggered to load the necessary historical messages. -It is recommended to fetch messages such that the target message would fall in the middle of the -loaded messages, i.e., if the target message id is 25 and page size is 20, then load messages +It is recommended to fetch messages such that the target message would fall in the middle of the +loaded messages, i.e., if the target message id is 25 and page size is 20, then load messages with ids from 15 to 35. #### Example: @@ -858,7 +864,6 @@ ChatView( ) ``` - ## Text Selection Config ```dart @@ -867,17 +872,17 @@ ChatView( textSelectionConfig: TextSelectionConfig( // Use platform-specific text selection controls (default is null for platform default) selectionControls: null, - + // Focus node for managing text selection focus focusNode: FocusNode(), - + // Callback triggered when text selection changes onSelectionChanged: (SelectedContent? content) { if (content != null) { debugPrint('Selected text: ${content.plainText}'); } }, - + // Customize the context menu shown during text selection contextMenuBuilder: (context, selectableRegionState) { return AdaptiveTextSelectionToolbar( @@ -897,10 +902,10 @@ ChatView( ], ); }, - + // Configure the magnifier shown during text selection magnifierConfiguration: const TextMagnifierConfiguration(), - + // Customize text selection theme (colors, handle size, etc.) themeData: const TextSelectionThemeData( cursorColor: Colors.blue, @@ -914,7 +919,7 @@ ChatView( ## Two-Way Pagination -ChatView supports two-way pagination for efficiently loading messages in both directions - +ChatView supports two-way pagination for efficiently loading messages in both directions - loading older messages when scrolling to the top and newer messages when scrolling to the bottom. This feature enables lazy loading and memory optimization for large chat histories. @@ -961,7 +966,7 @@ Widget build(BuildContext context) { limit: 20, ), }; - + // Add the loaded messages to the chat controller _chatController.loadMoreData( newMessages, @@ -979,7 +984,7 @@ Widget build(BuildContext context) { 2. **Error Handling**: Always handle API errors gracefully in your `loadMoreData` callback 3. **Loading States**: Use the built-in loading indicators or customize them for better UX 4. **Pagination State**: Properly manage `isLastPage` to prevent unnecessary API calls -5. **Reference Messages**: Use the provided reference message for cursor-based pagination for +5. **Reference Messages**: Use the provided reference message for cursor-based pagination for better performance ## Link Preview Configuration @@ -1018,7 +1023,7 @@ ChatView( flashingCircleBrightColor: Colors.grey, flashingCircleDarkColor: Colors.black, // For custom indicator - // padding: const EdgeInsets.only(left: 12), + // padding: const EdgeInsets.only(left: 12), // customIndicator: Container( // margin: const EdgeInsets.only(left: 8), // decoration: const BoxDecoration( @@ -1125,6 +1130,7 @@ ChatView( ``` ## Internationalization + ChatView supports internationalization (i18n) for various languages. You can set the locale using the `PackageString.setLocale('en')`. ```dart @@ -1151,51 +1157,65 @@ PackageStrings.addLocaleObject( PackageStrings.setLocale('es'); ``` -## Send Image With Message -You can send images along with your messages by enabling the `shouldSendImageWithText` flag in `sendMessageConfig` all the other things will be handled by the package itself. Here's how to do it: - -```dart -sendMessageConfig: SendMessageConfiguration( - shouldSendImageWithText: true, // Enable sending images with text -), -``` +## Image Preview and Sending Flow -You can also customize the view by using the `selectedImageViewBuilder` field of the `sendMessageConfig`: +You can customize the view for sending images by using the `GalleryActionButton` in `trailingActions`. Here's a practical example that opens an image preview screen before sending: ```dart sendMessageConfig: SendMessageConfiguration( - shouldSendImageWithText: true, - selectedImageViewBuilder: (images, onImageRemove) { - if (images.isNotEmpty) { - return SizedBox( - width: MediaQuery.sizeOf(context).width, - child: Stack( - children: [ - Image.file( - File(images.first), - height: 100, - ), - Positioned( - right: 0, - top: 0, - child: IconButton( - icon: const Icon( - Icons.close, + textFieldConfig: TextFieldConfiguration( + trailingActions: (context, controller) => [ + GalleryActionButton( + icon: Icon( + Icons.photo_rounded, + size: 30, + color: _theme.iconColor, + ), + onPressed: (path, replyMessage) { + if (path?.isEmpty ?? true) return; + // Open fullscreen image preview before sending + Navigator.of(context).push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => ImagePreviewScreen( + imagePath: path!, + replyMessage: replyMessage, + chatName: widget.chat.name, + onSend: (imagePath, caption, reply) { + // Create a timestamp for unique message IDs + final timeStamp = DateTime.now().microsecondsSinceEpoch; + // Add image message + _chatController.addMessage( + Message( + id: '${timeStamp}_img', + message: imagePath, + createdAt: DateTime.now(), + messageType: MessageType.image, + sentBy: _chatController.currentUser.id, + replyMessage: reply ?? const ReplyMessage(), + ), + ); + + // Add caption if provided + if (caption.isNotEmpty) { + _chatController.addMessage( + Message( + id: '${timeStamp}_cap', + message: caption, + createdAt: DateTime.now(), + messageType: MessageType.text, + sentBy: _chatController.currentUser.id, + ), + ); + } + }, ), - onPressed: () { - onImageRemove.call( - imagePath: images.first, - ); - }, - ), ), - ], - ), - ); - } else { - return const SizedBox.shrink(); - } - }, + ); + }, + ), + ], + ), ), ``` @@ -1205,10 +1225,10 @@ Easily connect Chatview UI to any backend using the [**Chatview Connect**](https package. It offers ready-to-use solutions for real-time messaging with supporting media uploads. - # Migration Guide ## Migration Guide for ChatView 3.0.0 + This guide will help you migrate your code from previous versions of ChatView to version 3.0.0. ## Key Changes @@ -1220,6 +1240,7 @@ encapsulates the recorder settings for both iOS and Android platforms. The `andr property has been removed so whatever format will be given by the encoder that will be used. Previous Usage: + ```dart ChatView( sendMessageConfig: SendMessageConfiguration( @@ -1234,6 +1255,7 @@ ChatView( ``` New Usage: + ```dart ChatView( sendMessageConfig: SendMessageConfiguration( @@ -1255,6 +1277,7 @@ ChatView( ### Text Field Action Items You can now add action buttons to the input field using two builders: + - `leadingActions`: widgets shown before the text field. - `trailingActions`: widgets shown after the text field. @@ -1310,7 +1333,7 @@ textFieldConfig: TextFieldConfiguration( ## Main Contributors | ![img](https://avatars.githubusercontent.com/u/25323183?v=4&s=200) | ![img](https://avatars.githubusercontent.com/u/56400956?v=4&s=200) | ![img](https://avatars.githubusercontent.com/u/65003381?v=4&s=200) | ![img](https://avatars.githubusercontent.com/u/41247722?v=4&s=200) | ![img](https://avatars.githubusercontent.com/u/72062416?v=4&s=200) | -|:------------------------------------------------------------------:|:------------------------------------------------------------------:|:------------------------------------------------------------------:|:------------------------------------------------------------------:|:------------------------------------------------------------------:| +| :----------------------------------------------------------------: | :----------------------------------------------------------------: | :----------------------------------------------------------------: | :----------------------------------------------------------------: | :----------------------------------------------------------------: | | [Vatsal Tanna](https://github.com/vatsaltanna) | [Ujas Majithiya](https://github.com/Ujas-Majithiya) | [Apurva Kanthraviya](https://github.com/apurva780) | [Aditya Chavda](https://github.com/aditya-chavda) | [Yash Dhrangdhariya](https://github.com/Yash-Dhrangdhariya) | ## How to Contribute diff --git a/example/lib/main.dart b/example/lib/main.dart index 99fa02a3..416356d7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -9,6 +9,7 @@ import 'models/chat_list_theme.dart'; import 'models/chatview_theme.dart'; import 'values/colors.dart'; import 'values/icons.dart'; +import 'widgets/image_preview_screen.dart'; void main() { runApp(const Example()); @@ -703,14 +704,40 @@ class _ExampleOneChatScreenState extends State { ), onPressed: (path, replyMessage) { if (path?.isEmpty ?? true) return; - _chatController.addMessage( - Message( - id: DateTime.now().millisecondsSinceEpoch.toString(), - message: path!, - createdAt: DateTime.now(), - messageType: MessageType.image, - sentBy: _chatController.currentUser.id, - replyMessage: replyMessage ?? const ReplyMessage(), + // Open fullscreen image preview before sending. + Navigator.of(context).push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => ImagePreviewScreen( + imagePath: path!, + replyMessage: replyMessage, + chatName: widget.chat.name, + onSend: (imagePath, caption, reply) { + final timeStamp = + DateTime.now().microsecondsSinceEpoch; + _chatController.addMessage( + Message( + id: '${timeStamp}_img', + message: imagePath, + createdAt: DateTime.now(), + messageType: MessageType.image, + sentBy: _chatController.currentUser.id, + replyMessage: reply ?? const ReplyMessage(), + ), + ); + if (caption.isNotEmpty) { + _chatController.addMessage( + Message( + id: '${timeStamp}_cap', + message: caption, + createdAt: DateTime.now(), + messageType: MessageType.text, + sentBy: _chatController.currentUser.id, + ), + ); + } + }, + ), ), ); }, diff --git a/example/lib/widgets/image_preview_screen.dart b/example/lib/widgets/image_preview_screen.dart new file mode 100644 index 00000000..0ace76e9 --- /dev/null +++ b/example/lib/widgets/image_preview_screen.dart @@ -0,0 +1,212 @@ +// Conditional import for dart:io (only available on non-web platforms) +// ignore: uri_does_not_exist +import 'dart:io'; + +import 'package:chatview/chatview.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; + +/// A full-screen image preview screen shown before sending an image. +/// +/// Displays the selected image full-screen with: +/// - A close button at the top-left +/// - A chat name in the top bar +/// - A caption text field + send button pinned to the bottom +class ImagePreviewScreen extends StatefulWidget { + const ImagePreviewScreen({ + super.key, + required this.imagePath, + required this.onSend, + this.replyMessage, + this.chatName, + }); + + /// Local file path of the selected image. + final String imagePath; + + /// Optional chat/contact name shown in the top bar. + final String? chatName; + + /// Active reply message carried over from the chat screen (may be null). + final ReplyMessage? replyMessage; + + /// Called when the user taps send. Provides the image path, caption text, + /// and the original reply message. + final void Function( + String imagePath, + String caption, + ReplyMessage? replyMessage, + ) onSend; + + @override + State createState() => _ImagePreviewScreenState(); +} + +class _ImagePreviewScreenState extends State { + final _captionController = TextEditingController(); + final _focusNode = FocusNode(); + + @override + void dispose() { + _captionController.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + void _send() { + widget.onSend( + widget.imagePath, + _captionController.text.trim(), + widget.replyMessage, + ); + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + + return Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.black, + body: SafeArea( + child: Stack( + children: [ + Positioned.fill( + child: InteractiveViewer( + maxScale: 4.0, + child: Center( + child: kIsWeb + ? Image.network( + widget.imagePath, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const Center( + child: Icon(Icons.broken_image, + color: Colors.white54, size: 64), + ), + ) + : Image.file( + File(widget.imagePath), + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const Center( + child: Icon(Icons.broken_image, + color: Colors.white54, size: 64), + ), + ), + ), + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: DecoratedBox( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black87, Colors.transparent], + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 8, + ), + child: Row( + children: [ + IconButton( + onPressed: () => Navigator.of(context).maybePop(), + icon: const Icon(Icons.close, color: Colors.white), + tooltip: 'Close', + ), + if (widget.chatName != null) ...[ + const SizedBox(width: 4), + Expanded( + child: Text( + 'Send to ${widget.chatName}', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ], + ), + ), + ), + ), + Positioned( + bottom: bottomInset, + left: 0, + right: 0, + child: DecoratedBox( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [Colors.black87, Colors.transparent], + ), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 40, 16, 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Caption text field + Expanded( + child: TextField( + controller: _captionController, + focusNode: _focusNode, + style: const TextStyle(color: Colors.white), + maxLines: 4, + minLines: 1, + textInputAction: TextInputAction.newline, + decoration: InputDecoration( + hintText: 'Add a caption…', + hintStyle: const TextStyle(color: Colors.white60), + filled: true, + fillColor: Colors.black45, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none, + ), + ), + ), + ), + + const SizedBox(width: 10), + + // Send button + SizedBox( + width: 52, + height: 52, + child: FloatingActionButton( + onPressed: _send, + backgroundColor: const Color(0xFF574FF0), + elevation: 4, + child: const Icon( + Icons.send_rounded, + color: Colors.white, + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/models/config_models/send_message_configuration.dart b/lib/src/models/config_models/send_message_configuration.dart index bcc586be..66ef99fc 100644 --- a/lib/src/models/config_models/send_message_configuration.dart +++ b/lib/src/models/config_models/send_message_configuration.dart @@ -31,7 +31,6 @@ import '../../values/typedefs.dart'; class SendMessageConfiguration { const SendMessageConfiguration({ this.voiceRecordingConfiguration = const VoiceRecordingConfiguration(), - this.shouldSendImageWithText = false, this.allowRecordingVoice = true, this.textFieldConfig, this.textFieldBackgroundColor, @@ -97,9 +96,6 @@ class SendMessageConfiguration { /// Configuration for cancel voice recording final CancelRecordConfiguration? cancelRecordConfiguration; - /// If true, then image will be sent with text message. - final bool shouldSendImageWithText; - /// Icon to remove image from text field. final Widget? removeImageIcon; diff --git a/lib/src/values/typedefs.dart b/lib/src/values/typedefs.dart index a0ecec67..158fa45b 100644 --- a/lib/src/values/typedefs.dart +++ b/lib/src/values/typedefs.dart @@ -37,7 +37,7 @@ typedef DoubleCallBack = void Function( double yPosition, double xPosition, ); -typedef StringsCallBack = void Function(String emoji, String messageId); +typedef StringsCallBack = void Function(String emoji); typedef StringWithReturnWidget = Widget Function(String separator); typedef DragUpdateDetailsCallback = void Function(DragUpdateDetails); typedef MoreTapCallBack = void Function( diff --git a/lib/src/widgets/chatui_textfield.dart b/lib/src/widgets/chatui_textfield.dart index 2d327583..26662c01 100644 --- a/lib/src/widgets/chatui_textfield.dart +++ b/lib/src/widgets/chatui_textfield.dart @@ -336,7 +336,6 @@ class _ChatUITextFieldState extends State { ? null : (path, _) => widget.onImageSelected( path ?? '', - '', ), ), GalleryActionButton( @@ -352,7 +351,6 @@ class _ChatUITextFieldState extends State { ? null : (path, _) => widget.onImageSelected( path ?? '', - '', ), ), ], diff --git a/lib/src/widgets/send_message_widget.dart b/lib/src/widgets/send_message_widget.dart index 7d1d0852..e42b9f2d 100644 --- a/lib/src/widgets/send_message_widget.dart +++ b/lib/src/widgets/send_message_widget.dart @@ -165,35 +165,18 @@ class SendMessageWidgetState extends State { builder: widget.replyMessageBuilder, onChange: (value) => _replyMessage = value, ), - if (widget - .sendMessageConfig.shouldSendImageWithText) - SelectedImageViewWidget( - key: _selectedImageViewWidgetKey, - sendMessageConfig: widget.sendMessageConfig, - ), + SelectedImageViewWidget( + key: _selectedImageViewWidgetKey, + sendMessageConfig: widget.sendMessageConfig, + ), ChatUITextField( focusNode: _focusNode, textEditingController: _textEditingController, onPressed: _onPressed, sendMessageConfig: widget.sendMessageConfig, onRecordingComplete: _onRecordingComplete, - onImageSelected: (images, messageId) { - if (widget.sendMessageConfig - .shouldSendImageWithText) { - if (images.isNotEmpty) { - _selectedImageViewWidgetKey.currentState - ?.selectedImages.value = [ - ...?_selectedImageViewWidgetKey - .currentState?.selectedImages.value, - images - ]; - - FocusScope.of(context) - .requestFocus(_focusNode); - } - } else { - _onImageSelected(images, ''); - } + onImageSelected: (imagePath) { + _onImageSelected(imagePath); }, ), ], @@ -219,7 +202,7 @@ class SendMessageWidgetState extends State { } } - void _onImageSelected(String imagePath, String error) { + void _onImageSelected(String imagePath) { if (imagePath.isEmpty) return; widget.onSendTap.call(imagePath, _replyMessage, MessageType.image); @@ -239,7 +222,7 @@ class SendMessageWidgetState extends State { if (_selectedImageViewWidgetKey.currentState?.selectedImages.value case final selectedImages?) { for (final image in selectedImages) { - _onImageSelected(image, ''); + _onImageSelected(image); } _selectedImageViewWidgetKey.currentState?.selectedImages.value = []; }