Skip to content
Draft
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
2 changes: 2 additions & 0 deletions lib/src/extensions/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ extension MessageTypes on MessageType {
bool get isVoice => this == MessageType.voice;

bool get isCustom => this == MessageType.custom;

bool get isFile => this == MessageType.file;
}

/// Extension on ConnectionState for checking specific connection.
Expand Down
9 changes: 7 additions & 2 deletions lib/src/models/config_models/send_message_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class SendMessageConfiguration {
this.selectedImageViewHeight,
this.imageBorderRadius,
this.selectedImageViewBuilder,
this.trailing, // <--- ADDED THIS LINE
});

/// Used to give background color to text field.
Expand Down Expand Up @@ -127,6 +128,10 @@ class SendMessageConfiguration {

/// Provides ability to build custom view for selected images in text field.
final SelectedImageViewBuilder? selectedImageViewBuilder;

/// Provides list of widgets that will be placed at the trailing end
/// of the text input field, typically used for custom action buttons.
final List<Widget>? trailing; // <--- ADDED THIS FIELD
}

class ImagePickerIconsConfiguration {
Expand Down Expand Up @@ -245,7 +250,7 @@ class ImagePickerConfiguration {
final CameraDevice? preferredCameraDevice;

/// Callback when image is picked from camera or gallery,
/// we can perform our task on image like adding crop options and return new image path
///  we can perform our task on image like adding crop options and return new image path
final ImagePickedCallback? onImagePicked;
}

Expand Down Expand Up @@ -329,4 +334,4 @@ class CancelRecordConfiguration {

/// Provides callback on voice record cancel
final VoidCallback? onCancel;
}
}
9 changes: 6 additions & 3 deletions lib/src/widgets/chatui_textfield.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class _ChatUITextFieldState extends State<ChatUITextField> {
VoiceRecordingConfiguration? get voiceRecordingConfig =>
widget.sendMessageConfig?.voiceRecordingConfiguration;

// FIX: Corrected getter name from imagePickerIconsConfiguration to imagePickerIconsConfig
ImagePickerIconsConfiguration? get imagePickerIconsConfig =>
sendMessageConfig?.imagePickerIconsConfig;

Expand Down Expand Up @@ -283,7 +284,7 @@ class _ChatUITextFieldState extends State<ChatUITextField> {
children: [
if (!isRecordingValue) ...[
if (sendMessageConfig?.enableCameraImagePicker ??
true)
true)
IconButton(
constraints: const BoxConstraints(),
onPressed: (textFieldConfig?.enabled ?? true)
Expand All @@ -302,7 +303,7 @@ class _ChatUITextFieldState extends State<ChatUITextField> {
),
),
if (sendMessageConfig?.enableGalleryImagePicker ??
true)
true)
IconButton(
constraints: const BoxConstraints(),
onPressed: (textFieldConfig?.enabled ?? true)
Expand Down Expand Up @@ -349,6 +350,8 @@ class _ChatUITextFieldState extends State<ChatUITextField> {
color: cancelRecordConfiguration?.iconColor ??
voiceRecordingConfig?.recorderIconColor,
),
if (sendMessageConfig?.trailing != null)
...sendMessageConfig!.trailing!,
],
);
}
Expand Down Expand Up @@ -451,4 +454,4 @@ class _ChatUITextFieldState extends State<ChatUITextField> {
});
_inputText.value = inputText;
}
}
}
220 changes: 220 additions & 0 deletions lib/src/widgets/file_message_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import 'package:flutter/material.dart';
import 'dart:io'; // Needed for File class
import 'package:chatview_utils/chatview_utils.dart'; // For Message model
import 'package:open_filex/open_filex.dart'; // Import open_filex
import 'package:url_launcher/url_launcher.dart'; // Import url_launcher
import 'package:flutter/foundation.dart' show kIsWeb; // Import kIsWeb

class FileMessageView extends StatelessWidget {
const FileMessageView({
Key? key,
required this.message,
required this.isMessageBySender,
}) : super(key: key);

final Message message;
final bool isMessageBySender;

// Function to check if a string is a valid HTTP/HTTPS URL
bool _isWebUrl(String urlString) {
try {
final uri = Uri.parse(urlString);
return uri.scheme == 'http' || uri.scheme == 'https';
} catch (e) {
return false;
}
}

void _onFileTap(BuildContext context, String pathOrUrl) async {
print('--- File Tap Debug Start ---');
print('1. Input path/URL: $pathOrUrl');

if (kIsWeb) {
// On web, always try to open as a URL
try {
final uri = Uri.parse(pathOrUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.platformDefault);
print('Opened URL on Web: $pathOrUrl');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Opened web link: ${uri.host}')),
);
} else {
print('Could not launch URL on Web: $pathOrUrl');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not open web link.')),
);
}
} catch (e) {
print('Error parsing or launching URL on Web: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Invalid URL or error opening: $pathOrUrl')),
);
}
print('--- File Tap Debug End (Web) ---');
return; // Exit as web handling is done
}

// For non-web platforms (mobile, desktop)
if (_isWebUrl(pathOrUrl)) {
print('2. Detected as Web URL. Attempting to open with url_launcher.');
try {
final uri = Uri.parse(pathOrUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication); // Open in external browser
print('Opened web URL: $pathOrUrl');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Opened web link: ${uri.host}')),
);
} else {
print('Could not launch web URL: $pathOrUrl');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not open web link: $pathOrUrl')),
);
}
} catch (e) {
print('Error parsing or launching URL: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Invalid URL or error opening: $pathOrUrl')),
);
}
} else {
// Assume it's a local file path
String actualFilePath;
if (pathOrUrl.startsWith('file:///')) {
actualFilePath = Uri.decodeComponent(pathOrUrl.substring('file:///'.length));
} else {
actualFilePath = Uri.decodeComponent(pathOrUrl);
}
print('2. Detected as Local File Path: $actualFilePath. Attempting to open with OpenFilex.');

final File file = File(actualFilePath);

if (await file.exists()) {
print('3. Local file exists at path: $actualFilePath');
try {
final OpenResult result = await OpenFilex.open(actualFilePath);
print('4. OpenFilex result type: ${result.type}');
print('5. OpenFilex result message: ${result.message}');

if (result.type == ResultType.done) {
print('File opened successfully!');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Opened file: ${file.path.split('/').last}')),
);
} else {
print('Failed to open local file with OpenFilex. Error: ${result.message}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not open file: ${file.path.split('/').last}. Error: ${result.message}')),
);
}
} catch (e) {
print('6. Exception caught when trying to open local file with OpenFilex: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('An unexpected error occurred: ${e.toString()}')),
);
}
} else {
print('3. Local file DOES NOT exist at path: $actualFilePath');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('File not found on device: ${file.path.split('/').last}')),
);
}
}
print('--- File Tap Debug End ---');
}

@override
Widget build(BuildContext context) {
final String pathOrUrl = message.message;

// Determine what to display based on whether it's a URL or a file path
String displayFileName;
IconData displayIcon;
Color iconColor = isMessageBySender ? Colors.blue[700]! : Colors.grey[700]!;
Color textColor = isMessageBySender ? Colors.blue[900]! : Colors.black87;

if (_isWebUrl(pathOrUrl)) {
displayFileName = Uri.parse(pathOrUrl).host; // Show domain for URL
displayIcon = Icons.link; // Link icon for URLs
} else {
// Treat as a local file path
String actualFilePath;
if (pathOrUrl.startsWith('file:///')) {
actualFilePath = Uri.decodeComponent(pathOrUrl.substring('file:///'.length));
} else {
actualFilePath = Uri.decodeComponent(pathOrUrl);
}
displayFileName = actualFilePath.split('/').last; // Show file name for local files
displayIcon = Icons.insert_drive_file; // Generic file icon
}

return GestureDetector(
onTap: () => _onFileTap(context, pathOrUrl),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
margin: EdgeInsets.only(
top: 6,
right: isMessageBySender ? 6 : 0,
left: isMessageBySender ? 0 : 6,
bottom: message.reaction.reactions.isNotEmpty ? 15 : 0,
),
decoration: BoxDecoration(
color: isMessageBySender ? Colors.blue[100] : Colors.grey[300],
borderRadius: BorderRadius.circular(14),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
displayIcon,
color: iconColor,
),
const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
displayFileName,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
// Conditional display for file size/status or URL status
_isWebUrl(pathOrUrl)
? Text(
'Web Link',
style: const TextStyle(fontSize: 12, color: Colors.black54),
)
: FutureBuilder<bool>(
future: File(pathOrUrl.startsWith('file:///') ? Uri.decodeComponent(pathOrUrl.substring('file:///'.length)) : Uri.decodeComponent(pathOrUrl)).exists(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData && snapshot.data == true) {
final file = File(pathOrUrl.startsWith('file:///') ? Uri.decodeComponent(pathOrUrl.substring('file:///'.length)) : Uri.decodeComponent(pathOrUrl));
return Text(
'File exists (${(file.lengthSync() / 1024).toStringAsFixed(2)} KB)',
style: const TextStyle(fontSize: 12, color: Colors.black54),
);
} else {
return Text(
'File not found',
style: const TextStyle(fontSize: 12, color: Colors.red),
);
}
}
return const Text('Checking file...', style: TextStyle(fontSize: 12, color: Colors.black54));
},
),
],
),
),
],
),
),
);
}
}
26 changes: 19 additions & 7 deletions lib/src/widgets/message_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import 'image_message_view.dart';
import 'reaction_widget.dart';
import 'text_message_view.dart';
import 'voice_message_view.dart';
import 'file_message_view.dart'; // ADD THIS IMPORT

class MessageView extends StatefulWidget {
const MessageView({
Expand Down Expand Up @@ -212,7 +213,18 @@ class _MessageViewState extends State<MessageView>
highlightImage: widget.shouldHighlight,
highlightScale: widget.highlightScale,
);
} else if (widget.message.messageType.isText) {
}
// ADDED THIS ELSE IF FOR MessageType.file
else if (widget.message.messageType == MessageType.file) {
return FileMessageView(
message: widget.message,
isMessageBySender: widget.isMessageBySender,
// You can add more configurations here if FileMessageView needs them
// fileMessageConfig: messageConfig?.fileMessageConfig, // Uncomment if you add a fileMessageConfig to MessageConfiguration
);
}
// END OF ADDED ELSE IF
else if (widget.message.messageType.isText) {
return TextMessageView(
inComingChatBubbleConfig: widget.inComingChatBubbleConfig,
outgoingChatBubbleConfig: widget.outgoingChatBubbleConfig,
Expand Down Expand Up @@ -252,11 +264,11 @@ class _MessageViewState extends State<MessageView>
.lastSeenAgoBuilderVisibility ??
true) {
return widget.outgoingChatBubbleConfig?.receiptsWidgetConfig
?.lastSeenAgoBuilder
?.call(
widget.message,
applicationDateFormatter(
widget.message.createdAt)) ??
?.lastSeenAgoBuilder
?.call(
widget.message,
applicationDateFormatter(
widget.message.createdAt)) ??
lastSeenAgoBuilder(widget.message,
applicationDateFormatter(widget.message.createdAt));
}
Expand All @@ -283,4 +295,4 @@ class _MessageViewState extends State<MessageView>
_animationController?.dispose();
super.dispose();
}
}
}
19 changes: 18 additions & 1 deletion lib/src/widgets/reply_message_type_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,23 @@ class ReplyMessageTypeView extends StatelessWidget {
),
],
),
// Case for MessageType.file - Corrected to use a hardcoded string for now
MessageType.file => Row(
children: [
Icon(
Icons.file_copy, // Or any other suitable file icon
size: 20,
color:
sendMessageConfig?.replyMessageColor ?? Colors.grey.shade700,
),
Text(
'File', // Changed from PackageStrings.currentLocale.file to a hardcoded string
style: TextStyle(
color: sendMessageConfig?.replyMessageColor ?? Colors.black,
),
),
],
),
MessageType.custom when customMessageReplyViewBuilder != null =>
customMessageReplyViewBuilder!(message),
MessageType.custom || MessageType.text => Text(
Expand All @@ -94,4 +111,4 @@ class ReplyMessageTypeView extends StatelessWidget {
),
};
}
}
}
Loading