onufitness_mobile/lib/screens/chat/views/chat_inside_screen.dart
2026-01-13 11:36:24 +05:30

1472 lines
46 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:onufitness/services/logger_service.dart';
import 'package:intl/intl.dart';
import 'package:agora_chat_sdk/agora_chat_sdk.dart';
import 'package:onufitness/constants/api_enum_constant.dart';
import 'package:onufitness/constants/asset_constants.dart';
import 'package:onufitness/constants/color_constant.dart';
import 'package:onufitness/constants/text_constant.dart';
import 'package:onufitness/screens/chat/controllers/chat_controller.dart';
import 'package:onufitness/screens/chat/widgets/chat_input_area.dart';
import 'package:onufitness/screens/chat/widgets/voice/voice_message_buble_widget.dart';
import 'package:onufitness/screens/echoboard/widget/image_zoom.dart';
import 'package:onufitness/screens/chat/widgets/chat_appbar.dart';
import 'package:onufitness/screens/chat/widgets/chat_video_widgets.dart';
import 'package:onufitness/services/agora/agora_chat_service.dart';
import 'package:onufitness/utils/custom_sneakbar.dart';
class ChatDetailScreen extends StatefulWidget {
final String targetUserId;
final String targetUserName;
final String? targetUserAvatar;
final bool isGroupMessage;
const ChatDetailScreen({
super.key,
required this.targetUserId,
required this.targetUserName,
this.targetUserAvatar,
this.isGroupMessage = false,
});
@override
State<ChatDetailScreen> createState() => _ChatDetailScreenState();
}
class _ChatDetailScreenState extends State<ChatDetailScreen> {
late ChatController chatCtrl;
late TextEditingController textController;
late ScrollController scrollController;
AgoraChatService agoraService = Get.put(AgoraChatService());
final logger = LoggerService();
// Track if we should maintain scroll position during pagination
bool maintainScrollPosition = false;
double scrollPosition = 0.0;
@override
void initState() {
if (!Get.isRegistered<ChatController>()) {
Get.put(ChatController());
}
chatCtrl = Get.find<ChatController>();
super.initState();
textController = TextEditingController();
scrollController = ScrollController();
chatCtrl.isGroupChattt.value = widget.isGroupMessage;
// Load existing messages for this conversation
WidgetsBinding.instance.addPostFrameCallback((_) {
// chatCtrl.isGroupChattt.value = widget.isGroupMessage;
initAgoraChatService();
isMyFriendCheck();
loadMessages();
// Set up listeners
setupControllers();
subscribeToUserPresence();
});
}
@override
void dispose() {
textController.dispose();
scrollController.dispose();
super.dispose();
}
initAgoraChatService() async {
if (agoraService.currentUserId == null) {
await Future.delayed(Duration(milliseconds: 500));
// Check again
if (agoraService.currentUserId == null) {
bool success = await agoraService.loginToAgora();
if (!success) {
customSnackbar(
title: "Error",
message: "Failed to connect to chat",
duration: 2,
);
Get.back();
return;
}
}
}
}
subscribeToUserPresence() async {
await chatCtrl.subscribeToUserPresence(widget.targetUserId);
}
isMyFriendCheck() async {
await chatCtrl.friendCheck(connectedAgoraUserID: widget.targetUserId);
}
Future<void> loadMessages() async {
// Load messages from the conversation
await chatCtrl.loadMessages(widget.targetUserId);
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
void setupControllers() {
// For tracking scroll direction
scrollController.addListener(() {
final position = scrollController.position;
// Detect upward scroll near top to trigger pagination
if (position.userScrollDirection == ScrollDirection.forward &&
position.pixels <= 100) {
_loadMoreMessages();
}
});
// ONLY scroll to bottom when a new message is added at the end
ever(chatCtrl.messagesMap, (map) {
final messages = map[widget.targetUserId] ?? [];
// Only scroll to bottom if this is a NEW message appended
if (messages.isNotEmpty) {
final lastMsg = messages.last;
final isNewMessage =
DateTime.fromMillisecondsSinceEpoch(
lastMsg.serverTime,
).difference(DateTime.now()).inSeconds.abs() <
5;
if (isNewMessage) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
}
});
}
Future<void> _loadMoreMessages() async {
final hasMore = chatCtrl.hasMoreMessages[widget.targetUserId] ?? false;
final isLoading = chatCtrl.isLoadingMore[widget.targetUserId] ?? false;
if (hasMore && !isLoading) {
// Save current scroll position
scrollPosition = scrollController.position.pixels;
// maintainScrollPosition = true;
await chatCtrl.loadMoreMessages(widget.targetUserId);
}
}
void _scrollToBottom() {
if (scrollController.hasClients) {
Future.delayed(const Duration(milliseconds: 100), () {
if (scrollController.hasClients &&
scrollController.position.maxScrollExtent > 0) {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (!didPop) {
Get.back();
await chatCtrl.markAllMessagesAsRead(widget.targetUserId);
chatCtrl.selectedUserId.value = "";
await chatCtrl.unsuscribePresence(widget.targetUserId);
}
},
child: Scaffold(
resizeToAvoidBottomInset: true,
body: Container(
decoration: BoxDecoration(
color: Color(primaryColor),
image: DecorationImage(
image: AssetImage(AssetConstants.chatAppBarBackgroundImage),
fit: BoxFit.cover,
opacity: 0.5,
),
),
child: SafeArea(
bottom: false,
child: Column(
mainAxisSize: MainAxisSize.max, // For Samsung phones
children: [
// Reactive app bar
Obx(() {
final status =
chatCtrl.presenceMap[widget.targetUserId] ?? '';
final isOnline = status == 'online';
return chatAppBar(
context: context,
chatCtrl: chatCtrl,
receiverName: widget.targetUserName,
showOnlineDot: isOnline,
targetUserAvatarUrl: widget.targetUserAvatar,
userId: widget.targetUserId,
isGroupChat: widget.isGroupMessage,
);
}),
// Chat messages
Expanded(
child: ChatMessageList(
chatCtrl: chatCtrl,
scrollController: scrollController,
targetUserId: widget.targetUserId,
isGroupMessage: widget.isGroupMessage,
),
),
// File upload loader
FileUploadLoader(
targetUserId: widget.targetUserId,
chatCtrl: chatCtrl,
),
// Conditionally show chat input
if (widget.isGroupMessage == true)
Column(
children: [
ChatInputAreaWithVoice(
chatCtrl: chatCtrl,
textController: textController,
targetUserId: widget.targetUserId,
isGroupMessage: widget.isGroupMessage,
targetUserName: widget.targetUserName,
),
Container(color: Colors.white, height: 20.h),
],
)
else
Obx(() {
if (chatCtrl.isMyFriendCheckLoading.value) {
// Using this Insted of Loader ( for better UX)
return Column(
children: [
ChatInputAreaWithVoice(
chatCtrl: chatCtrl,
textController: textController,
targetUserId: widget.targetUserId,
isGroupMessage: widget.isGroupMessage,
targetUserName: widget.targetUserName,
),
],
);
}
if (chatCtrl.isMyFriend.value) {
return Column(
children: [
ChatInputAreaWithVoice(
chatCtrl: chatCtrl,
textController: textController,
targetUserId: widget.targetUserId,
isGroupMessage: widget.isGroupMessage,
targetUserName: widget.targetUserName,
),
],
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.only(bottom: 20.sp, top: 15.sp),
child: Text(
"You cannot send messages to this user right now.",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black),
),
),
Container(
height: MediaQuery.of(context).padding.bottom,
),
],
);
}),
],
),
),
),
),
);
}
}
// Message List Component with Pagination Support
class ChatMessageList extends StatelessWidget {
final ChatController chatCtrl;
final ScrollController scrollController;
final String targetUserId;
final bool isGroupMessage;
const ChatMessageList({
super.key,
required this.chatCtrl,
required this.scrollController,
required this.targetUserId,
this.isGroupMessage = false,
});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: Column(
children: [
// End-to-End Encryption Notice
const EncryptionNotice(),
// Message List
Expanded(child: Obx(() => messageListContent())),
],
),
);
}
Widget messageListContent() {
final messages = chatCtrl.messagesMap[targetUserId] ?? [];
final isLoadingMore = chatCtrl.isLoadingMore[targetUserId] ?? false;
final hasMoreMessages = chatCtrl.hasMoreMessages[targetUserId] ?? false;
if (messages.isEmpty && !isLoadingMore) {
return const EmptyChat();
}
return ListView.builder(
controller: scrollController,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20),
itemCount: messages.length + (isLoadingMore ? 1 : 0),
itemBuilder: (_, i) {
// Show loading indicator at the top
if (i == 0 && isLoadingMore) {
return const LoadingIndicator();
}
final messageIndex = isLoadingMore ? i - 1 : i;
final msg = messages[messageIndex];
final showDate =
messageIndex == 0 ||
!_isSameDay(
DateTime.fromMillisecondsSinceEpoch(
messages[messageIndex - 1].serverTime,
),
DateTime.fromMillisecondsSinceEpoch(msg.serverTime),
);
final isGroupMsg = isGroupMessage;
final isMe = msg.from == ChatClient.getInstance.currentUserId;
// Get sender name for group messages
String? senderName;
if (isGroupMsg && !isMe) {
// First try to get from userInfoMap
final userInfo = chatCtrl.userInfoMap[msg.from];
if (userInfo != null && userInfo.nickName?.isNotEmpty == true) {
senderName = userInfo.nickName;
} else if (userInfo != null && userInfo.userId.isNotEmpty == true) {
senderName = userInfo.userId;
} else {
// Fallback to sender's user ID if no display name is available
// senderName = msg.from ?? 'Unknown';
senderName = 'Unknown';
// Trigger fetch of user info for future messages
if (msg.from != null) {
chatCtrl.fetchUserInfo(msg.from!);
}
}
}
return Column(
children: [
if (showDate)
_DateSeparator(
date: DateTime.fromMillisecondsSinceEpoch(msg.serverTime),
),
CustomMessageBubble(
message: msg,
isMe: isMe,
isGroupMessage: isGroupMsg,
senderName: senderName,
chatController: chatCtrl,
),
// Show "Load All Messages" button at the top (after loading indicator)
if (messageIndex == 0 && !isLoadingMore && hasMoreMessages)
LoadAllMessagesButton(
chatCtrl: chatCtrl,
targetUserId: targetUserId,
),
],
);
},
);
}
bool _isSameDay(DateTime date1, DateTime date2) {
return date1.year == date2.year &&
date1.month == date2.month &&
date1.day == date2.day;
}
}
// End-to-End Encryption Notice Widget
class EncryptionNotice extends StatelessWidget {
const EncryptionNotice({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(color: Colors.white),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.lock, size: regularSizeText, color: lightGreyColor),
const SizedBox(width: 8),
Text(
"End to End Encrypted",
style: TextStyle(
fontSize: smallSizeText,
color: lightGreyColor,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}
// Loading Indicator Widget
class LoadingIndicator extends StatelessWidget {
const LoadingIndicator({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(height: 8),
Text(
"Loading messages...",
style: TextStyle(color: Colors.grey, fontSize: 12),
),
],
),
),
);
}
}
// Load All Messages Button Widget
class LoadAllMessagesButton extends StatelessWidget {
final ChatController chatCtrl;
final String targetUserId;
const LoadAllMessagesButton({
super.key,
required this.chatCtrl,
required this.targetUserId,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(16),
child: Center(
child: ElevatedButton.icon(
onPressed: () async {
await chatCtrl.loadAllMessages(targetUserId);
},
icon: const Icon(Icons.history, size: 16),
label: const Text("Load All Messages"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade50,
foregroundColor: Colors.blue.shade700,
elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
),
),
);
}
}
class EmptyChat extends StatelessWidget {
const EmptyChat({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.chat_bubble_outline,
size: 80,
color: Colors.grey.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
const Text(
"No messages yet",
style: TextStyle(color: Colors.grey, fontSize: 16),
),
const SizedBox(height: 8),
Text(
"Say hello to start the conversation!",
style: TextStyle(color: Colors.grey.shade600, fontSize: 14),
),
],
),
);
}
}
class _DateSeparator extends StatelessWidget {
final DateTime date;
const _DateSeparator({required this.date});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
Expanded(child: Divider(color: Colors.grey.shade300)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(12),
),
child: Text(
_formatMessageDate(date),
style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
),
),
),
Expanded(child: Divider(color: Colors.grey.shade300)),
],
),
);
}
String _formatMessageDate(DateTime date) {
final now = DateTime.now();
if (_isSameDay(date, now)) {
return "Today";
} else if (_isSameDay(date, now.subtract(const Duration(days: 1)))) {
return "Yesterday";
} else if (now.difference(date).inDays < 7) {
return DateFormat('EEEE').format(date);
} else {
return DateFormat('MMM d, yyyy').format(date);
}
}
bool _isSameDay(DateTime date1, DateTime date2) {
return date1.year == date2.year &&
date1.month == date2.month &&
date1.day == date2.day;
}
}
class CustomMessageBubble extends StatelessWidget {
final ChatMessage message;
final bool isMe;
final bool isGroupMessage;
final String? senderName;
final ChatController chatController;
const CustomMessageBubble({
super.key,
required this.message,
required this.isMe,
required this.chatController,
this.isGroupMessage = false,
this.senderName,
});
@override
Widget build(BuildContext context) {
final timeString = DateFormat(
'h:mm a',
).format(DateTime.fromMillisecondsSinceEpoch(message.serverTime));
return Align(
alignment: isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
constraints: BoxConstraints(maxWidth: Get.width * 0.75),
margin: EdgeInsets.only(
top: 4.h,
bottom: 6.h,
left: isMe ? 30.w : 15.w,
right: isMe ? 15.w : 30.w,
),
child: Column(
crossAxisAlignment:
isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: isMe ? Color(chatBubbleColor) : Colors.grey.shade200,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(isMe ? 20 : 2),
topRight: Radius.circular(isMe ? 2 : 20),
bottomLeft: const Radius.circular(20),
bottomRight: const Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
border:
isMe
? null
: Border.all(
color: Colors.grey.withValues(alpha: 0.1),
width: 1,
),
),
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: getPaddingHorizontal(),
vertical: getPaddingVertical(),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Sender name for group messages
if (isGroupMessage && !isMe && senderName != null)
Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Text(
senderName!,
style: TextStyle(
fontSize: verySmallSizeText,
color: deeperShadeOfprimaryColor,
fontWeight: FontWeight.w600,
),
),
),
// Message content based on type
messageContentWidget(chatController),
const SizedBox(height: 6),
// Time and status row
Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Text(
timeString,
style: TextStyle(
fontSize: verySmallSizeText,
color: isMe ? Colors.grey : Color(darkGreyColor),
fontWeight: FontWeight.w400,
),
),
if (isMe && !isGroupMessage) ...[
const SizedBox(width: 4),
messageStatusIconWidget(),
],
],
),
],
),
),
),
],
),
),
);
}
double getPaddingHorizontal() {
switch (message.body.type) {
case MessageType.IMAGE:
return 8.w;
case MessageType.FILE:
return 10.w;
default:
return 15.w;
}
}
double getPaddingVertical() {
switch (message.body.type) {
case MessageType.IMAGE:
return 8.h;
case MessageType.FILE:
return 10.w;
default:
return 5.h;
}
}
Widget messageContentWidget(ChatController chatController) {
final messageType = message.body.type;
// Handle text messages
if (messageType == MessageType.TXT) {
return textMessageWidget();
}
// Handle voice messages
if (messageType == MessageType.VOICE) {
return VoiceMessageBubble(
message: message,
isMe: isMe,
chatController: chatController,
);
}
// Handle file-based messages
if (message.body is! ChatFileMessageBody) {
return unsupportedMessageWidget();
}
final body = message.body as ChatFileMessageBody;
final fileName = body.displayName?.toLowerCase().trim() ?? '';
final extension = chatController.getFileExtension(fileName);
if (chatController.isImageExtension(extension)) {
return imageMessageWidget();
} else if (chatController.isVideoExtension(extension)) {
return videoMessageWidget();
} else if (chatController.isDocumentExtension(extension)) {
return fileMessageWidget();
} else {
return unsupportedMessageWidget();
}
}
Widget textMessageWidget() {
final textBody = message.body as ChatTextMessageBody;
return Text(
textBody.content,
style: TextStyle(
fontSize: 16,
color: isMe ? Colors.black : Colors.black,
height: 1.3,
),
);
}
Widget imageMessageWidget() {
final imageBody = message.body as ChatFileMessageBody;
final chatCtrl = Get.find<ChatController>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
constraints: BoxConstraints(
maxWidth: Get.width * 0.6,
maxHeight: 300,
minWidth: 150,
minHeight: 150,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Stack(
children: [
customImageContainer(imageBody),
Positioned(
top: 8,
right: 8,
child: Obx(() {
final isDownloading =
chatCtrl.isDownloading[message.msgId] ?? false;
final progress =
chatCtrl.downloadProgress[message.msgId] ?? 0.0;
return Container(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(30),
),
child:
isDownloading
? Container(
padding: const EdgeInsets.all(8),
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: progress,
strokeWidth: 2,
color: Colors.white,
),
),
)
: IconButton(
onPressed: () => chatCtrl.downloadFile(message),
icon: const Icon(
Icons.download,
color: Colors.white,
size: 18,
),
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
),
);
}),
),
],
),
),
),
],
);
}
Widget customImageContainer(ChatFileMessageBody imageBody) {
if (imageBody.localPath.isNotEmpty == true) {
final localFile = File(imageBody.localPath);
if (localFile.existsSync()) {
return GestureDetector(
onTap:
() => Get.to(
() => ImageZoomWidget(
imageUrl: imageBody.remotePath!,
heroTag: "Hey_hero_${DateTime.now().millisecondsSinceEpoch}",
),
),
child: Image.file(
localFile,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) =>
defaultImagePlaceholder(imageBody),
),
);
}
}
// Try to display remote image
if (imageBody.remotePath?.isNotEmpty == true) {
return GestureDetector(
onTap:
() => Get.to(
() => ImageZoomWidget(
imageUrl: imageBody.remotePath!,
heroTag: "${DateTime.now().millisecondsSinceEpoch}",
),
),
child: Image.network(
imageBody.remotePath!,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value:
loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
errorBuilder:
(context, error, stackTrace) =>
defaultImagePlaceholder(imageBody),
),
);
}
return defaultImagePlaceholder(imageBody);
}
Widget defaultImagePlaceholder(ChatFileMessageBody imageBody) {
return Container(
width: double.infinity,
height: 150,
color: Colors.grey.shade300,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.image, size: 40, color: Colors.grey.shade600),
const SizedBox(height: 8),
Text(
imageBody.displayName ?? 'Image',
style: TextStyle(color: Colors.grey.shade600),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
],
),
);
}
Widget videoMessageWidget() {
final videoBody = message.body as ChatFileMessageBody;
final chatCtrl = Get.find<ChatController>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
constraints: BoxConstraints(
maxWidth: Get.width * 0.6,
maxHeight: 150,
minWidth: 150,
minHeight: 150,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Stack(
children: [
Container(
width: double.infinity,
height: 150.h,
color: Colors.black,
),
// Play button overlay
Positioned(
top: 0,
right: 0,
bottom: 0,
left: 0,
child: Container(
color: Colors.black.withValues(alpha: 0.3),
child: Center(
child: GestureDetector(
onTap: () => playVideo(videoBody),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
shape: BoxShape.circle,
),
child: const Icon(
Icons.play_arrow,
color: Colors.white,
size: 30,
),
),
),
),
),
),
// Download overlay if needed
Obx(() {
final isDownloading =
chatCtrl.isDownloading[message.msgId] ?? false;
final progress =
chatCtrl.downloadProgress[message.msgId] ?? 0.0;
if (isDownloading) {
return Container(
color: Colors.black.withValues(alpha: 0.5),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
value: progress,
color: Colors.white,
),
const SizedBox(height: 8),
Text(
'${(progress * 100).toInt()}%',
style: const TextStyle(color: Colors.white),
),
],
),
),
);
}
return const SizedBox.shrink();
}),
],
),
),
),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: Text(
videoBody.displayName ?? 'Video',
style: TextStyle(
fontSize: verySmallSizeText,
color: isMe ? Colors.black : Colors.black,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 10),
// Download button beside video file name
Obx(() {
final isDownloading =
chatCtrl.isDownloading[message.msgId] ?? false;
return GestureDetector(
onTap:
isDownloading ? null : () => chatCtrl.downloadFile(message),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 5,
),
decoration: BoxDecoration(
color: isDownloading ? Colors.grey : Colors.blue,
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isDownloading ? Icons.downloading : Icons.download,
size: regularSizeText,
color: Colors.white,
),
],
),
),
);
}),
],
),
],
);
}
void playVideo(ChatFileMessageBody videoBody) async {
String? videoPath;
// Check if video is available locally
if (videoBody.localPath.isNotEmpty == true &&
File(videoBody.localPath).existsSync()) {
videoPath = videoBody.localPath;
} else if (videoBody.remotePath?.isNotEmpty == true) {
videoPath = videoBody.remotePath!;
}
if (videoPath != null) {
Get.to(() => VideoPlayerScreen(videoPath: videoPath!));
} else {
// Show download dialog
Get.dialog(
AlertDialog(
title: const Text('Video Not Available'),
content: const Text('Please download the video first to play it.'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
Get.back();
final chatCtrl = Get.find<ChatController>();
chatCtrl.downloadFile(message);
},
child: const Text('Download'),
),
],
),
);
}
}
// Updated fileMessageWidget method for PDF and other file handling
Widget fileMessageWidget() {
final fileBody = message.body as ChatFileMessageBody;
final chatCtrl = Get.find<ChatController>();
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color:
isMe ? Colors.white.withValues(alpha: 0.2) : Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: getFileIconColor(fileBody.displayName ?? ''),
borderRadius: BorderRadius.circular(6),
),
child: Icon(
getFileIcon(fileBody.displayName ?? ''),
color: Colors.white,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fileBody.displayName ?? 'Unknown File',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isMe ? Colors.black87 : Colors.black,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
_formatFileSize(fileBody.fileSize!),
style: TextStyle(
fontSize: verySmallSizeText,
color: isMe ? Colors.black : Colors.black,
),
),
],
),
),
// Download button only
Obx(() {
final isDownloading =
chatCtrl.isDownloading[message.msgId] ?? false;
final progress =
chatCtrl.downloadProgress[message.msgId] ?? 0.0;
if (isDownloading) {
return SizedBox(
width: 40,
height: 40,
child: Stack(
alignment: Alignment.center,
children: [
CircularProgressIndicator(
value: progress,
strokeWidth: 2,
backgroundColor: Colors.grey.shade300,
),
Text(
'${(progress * 100).toInt()}%',
style: const TextStyle(fontSize: 8),
),
],
),
);
}
return GestureDetector(
onTap: () => chatCtrl.downloadFile(message),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(6),
),
child: Icon(Icons.download, color: Colors.white, size: 16),
),
);
}),
],
),
],
),
);
}
// New VideoPlayerScreen widget
Widget unsupportedMessageWidget() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.shade300),
),
child: Row(
children: [
Icon(Icons.warning, color: Colors.orange.shade700),
const SizedBox(width: 8),
Expanded(
child: Text(
'Unsupported message type: ${message.body.type}',
style: TextStyle(color: Colors.orange.shade700, fontSize: 12),
),
),
],
),
);
}
Widget messageStatusIconWidget() {
if (message.hasReadAck) {
return Icon(Icons.done_all, size: 14, color: Colors.blue);
} else if (message.hasDeliverAck) {
return Icon(Icons.done_all, size: 14, color: Colors.grey);
} else {
return Icon(Icons.check, size: 14, color: Colors.grey);
}
}
IconData getFileIcon(String fileName) {
final extension = fileName.split('.').last.toLowerCase();
switch (extension) {
case 'pdf':
return Icons.picture_as_pdf;
case 'doc':
case 'docx':
return Icons.description;
case 'xls':
case 'xlsx':
return Icons.table_chart;
case 'ppt':
case 'pptx':
return Icons.slideshow;
case 'zip':
case 'rar':
return Icons.archive;
case 'txt':
return Icons.text_snippet;
default:
return Icons.insert_drive_file;
}
}
Color getFileIconColor(String fileName) {
final extension = fileName.split('.').last.toLowerCase();
switch (extension) {
case 'pdf':
return Colors.red;
case 'doc':
case 'docx':
return Colors.blue;
case 'xls':
case 'xlsx':
return Colors.green;
case 'ppt':
case 'pptx':
return Colors.orange;
case 'zip':
case 'rar':
return Colors.purple;
case 'txt':
return Colors.grey;
default:
return Colors.blueGrey;
}
}
String _formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
}
class SendButton extends StatelessWidget {
final ChatController chatCtrl;
final TextEditingController textController;
final String targetUserId;
final bool isGroupMessage;
final String targetUserName;
const SendButton({
super.key,
required this.chatCtrl,
required this.textController,
required this.targetUserId,
this.isGroupMessage = false,
required this.targetUserName,
});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<TextEditingValue>(
valueListenable: textController,
builder: (context, value, child) {
return GestureDetector(
onTap: () {
_sendMessage();
},
onLongPress: () {},
child: Container(
width: 45.w,
height: 45.h,
decoration: BoxDecoration(
color: const Color(primaryColor),
shape: BoxShape.circle,
),
child: Icon(Icons.send, color: Colors.black),
),
);
},
);
}
void _sendMessage() {
final text = textController.text.trim();
if (text.isNotEmpty) {
chatCtrl.sendTextMessage(targetUserId, text, isGroup: isGroupMessage);
textController.clear();
if (isGroupMessage == true) {
chatCtrl.sendGroupChatNotification(
notificationMessage: text,
notificationTitle: "",
notificationType: ApiEnum.ChatNotification,
tribeId: targetUserId,
);
}
if (!isGroupMessage) {
chatCtrl.sendChatNotification(
notificationMessage: text,
notificationTitle: "",
notificationType: ApiEnum.ChatNotification,
receiverId: targetUserId,
);
}
}
}
}
class FileUploadLoader extends StatelessWidget {
final String targetUserId;
final ChatController chatCtrl;
const FileUploadLoader({
super.key,
required this.targetUserId,
required this.chatCtrl,
});
@override
Widget build(BuildContext context) {
return Obx(() {
final isUploading = chatCtrl.isSending[targetUserId] ?? false;
return AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
transitionBuilder: (child, animation) {
return SizeTransition(sizeFactor: animation, child: child);
},
child:
isUploading
? Container(
key: const ValueKey("file_uploading"),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
color: const Color(primaryColor).withValues(alpha: 0.1),
border: Border(
top: BorderSide(color: Colors.grey.shade300),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
offset: const Offset(0, -2),
blurRadius: 6,
),
],
),
child: Padding(
padding: EdgeInsets.all(6),
child: Row(
children: [
SizedBox(
width: 24.w,
height: 24.h,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: const Color(primaryColor),
backgroundColor: const Color(
primaryColor,
).withValues(alpha: 0.3),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
"Sending file...",
style: TextStyle(
color: const Color(primaryColor),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
// Tooltip(
// message: "Cancel upload",
// child: IconButton(
// onPressed: () {
// chatCtrl.isSending[targetUserId] = false;
// },
// icon: const Icon(Icons.close, size: 20),
// color: Colors.grey.shade600,
// constraints: const BoxConstraints(
// minWidth: 32,
// minHeight: 32,
// ),
// ),
// ),
],
),
),
)
: const SizedBox.shrink(),
);
});
}
}