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..09bb2fbc 100644 --- a/doc/documentation.md +++ b/doc/documentation.md @@ -1151,51 +1151,66 @@ 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: +## Send Images -```dart -sendMessageConfig: SendMessageConfiguration( - shouldSendImageWithText: true, // Enable sending images with text -), -``` - -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 + var 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(); - } - }, + ); + }, + ), + ], + ), ), ``` diff --git a/example/lib/main.dart b/example/lib/main.dart index 99fa02a3..6db40d62 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) { + var 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..1378df5a --- /dev/null +++ b/example/lib/widgets/image_preview_screen.dart @@ -0,0 +1,201 @@ +import 'dart:io'; + +import 'package:chatview/chatview.dart'; +import 'package:flutter/material.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( + minScale: 0.8, + maxScale: 4.0, + child: Center( + child: 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/widgets/send_message_widget.dart b/lib/src/widgets/send_message_widget.dart index 7d1d0852..36c2e14a 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, messageId) { + _onImageSelected(imagePath, messageId); }, ), ], @@ -219,7 +202,7 @@ class SendMessageWidgetState extends State { } } - void _onImageSelected(String imagePath, String error) { + void _onImageSelected(String imagePath, String messageId) { if (imagePath.isEmpty) return; widget.onSendTap.call(imagePath, _replyMessage, MessageType.image);