taxgilde/lib/view/Mahi_chat/live_chat_screen.dart
2026-04-15 12:32:30 +05:30

1017 lines
37 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:get/get.dart';
import 'package:taxglide/consts/app_asstes.dart';
import 'package:taxglide/consts/notification_webscoket.dart';
import 'package:taxglide/controller/api_contoller.dart';
import 'package:taxglide/model/chat_model.dart';
import 'package:taxglide/view/Mahi_chat/chat_profile_screen.dart';
import 'package:taxglide/view/Mahi_chat/comman_input_button.dart';
import 'package:taxglide/view/Mahi_chat/file_images_screen.dart';
import 'package:taxglide/view/Mahi_chat/webscoket.dart';
// ⭐ Provider to hold the tagged message
final taggedMessageProvider = StateProvider<MessageModel?>((ref) => null);
// ⭐ Global key map to track message positions (if you want from outside)
final messageKeysProvider = StateProvider<Map<int, GlobalKey>>((ref) => {});
class LiveChatScreen extends ConsumerStatefulWidget {
final int chatid;
final String? fileid;
const LiveChatScreen({super.key, required this.chatid, required this.fileid});
@override
ConsumerState<LiveChatScreen> createState() => _LiveChatScreenState();
}
class _LiveChatScreenState extends ConsumerState<LiveChatScreen>
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
@override
bool get wantKeepAlive => true;
final ScrollController scrollController = ScrollController();
final ChatWebSocketService _wsService = ChatWebSocketService();
bool showScrollButton = false;
bool userIsReading = false;
bool _isPaginationLoading = false;
bool _hasNewMessage = false;
bool _isOtherUserTyping = false;
Timer? _typingTimeoutTimer;
// For reply scroll - FIXED: Use unique key per message
final Map<String, GlobalKey> _messageKeys = {};
// ⭐⭐⭐ Key variables for smooth pagination
final double _paginationTriggerOffset = 300; // Start loading 300px before top
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// ✅ Tell the global notification WS that we are viewing this chat,
// so it suppresses banner notifications for this specific chat.
NotificationWebSocket().setActiveChatId(widget.chatid.toString());
_initializeWebSocket();
scrollController.addListener(_handleScroll);
// Refresh immediately in background if already loaded
Future.microtask(() {
final currentState = ref.read(chatMessagesProvider(widget.chatid));
if (currentState is AsyncData) {
ref.read(chatMessagesProvider(widget.chatid).notifier).refresh();
}
});
}
// ⭐ WhatsApp-like Smooth Pagination Logic
void _handleScroll() {
if (!scrollController.hasClients) return;
double position = scrollController.position.pixels;
double maxExtent = scrollController.position.maxScrollExtent;
// In a reversed list, maxExtent is the "top" (older messages)
if (position >= maxExtent - _paginationTriggerOffset &&
!_isPaginationLoading) {
_triggerPagination();
}
// Show scroll to bottom button
// In a reversed list, bottom is 0
if (position > 100) {
if (!showScrollButton) {
setState(() => showScrollButton = true);
}
} else {
if (showScrollButton) {
setState(() => showScrollButton = false);
}
}
}
// ⭐ Simplified pagination for reversed list
Future<void> _triggerPagination() async {
final notifier = ref.read(chatMessagesProvider(widget.chatid).notifier);
if (!notifier.hasNextPage || _isPaginationLoading) return;
setState(() => _isPaginationLoading = true);
try {
await notifier.loadMessages(); // Prepends older messages naturally
} catch (e) {
debugPrint("❌ Pagination error: $e");
} finally {
if (mounted) setState(() => _isPaginationLoading = false);
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
debugPrint("📱 App Lifecycle State: $state");
if (state == AppLifecycleState.resumed) {
debugPrint("🔄 App resumed - checking for new messages");
if (_hasNewMessage) {
_silentRefresh();
_hasNewMessage = false;
}
}
}
Future<void> _silentRefresh() async {
try {
debugPrint("🔄 Starting silent refresh...");
await ref.read(chatMessagesProvider(widget.chatid).notifier).refresh();
debugPrint("✅ Silent refresh completed");
// In a reversed list, new messages are at 0. No manual scroll needed.
// The reverse: true property handles this naturally.
if (mounted) {
setState(() {});
}
} catch (e) {
debugPrint("❌ Error during silent refresh: $e");
}
}
Future<void> _initializeWebSocket() async {
try {
_wsService.onMessageReceived = (Map<String, dynamic> message) async {
debugPrint("📨 WebSocket message received: $message");
try {
final event = message['event'] as String?;
if (event == 'message.sent' ||
event == 'message.received' ||
event == 'message.read' ||
event == 'message.delivered') {
debugPrint("🔔 Message status/new message event: $event");
if (WidgetsBinding.instance.lifecycleState ==
AppLifecycleState.resumed) {
debugPrint("✅ Screen active - refreshing now");
await _silentRefresh();
} else {
debugPrint("⏳ Screen not active - will refresh on resume");
_hasNewMessage = true;
}
// Notification banner is handled centrally in NotificationWebSocket.
// No need to show it here it is already suppressed for this chat.
}
if (event == 'client-typing') {
debugPrint("⌨️ Typing event received: ${message['data']}");
_handleTypingEvent(message['data']);
}
} catch (e) {
debugPrint("❌ Error handling WebSocket message: $e");
}
};
_wsService.onConnected = () {
debugPrint("✅ WebSocket Connected");
};
_wsService.onDisconnected = () {
debugPrint("🔌 WebSocket Disconnected");
};
_wsService.onError = (error) {
debugPrint("❌ WebSocket Error: $error");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Connection error: $error"),
backgroundColor: Colors.red,
duration: const Duration(seconds: 2),
),
);
}
};
await _wsService.connect(chatId: widget.chatid.toString());
} catch (e) {
debugPrint("❌ Error initializing WebSocket: $e");
}
}
@override
void dispose() {
// ✅ Clear active chat so notifications resume when user leaves this screen.
NotificationWebSocket().clearActiveChatId();
WidgetsBinding.instance.removeObserver(this);
_wsService.disconnect();
scrollController.dispose();
super.dispose();
}
void _handleTypingEvent(dynamic data) {
try {
Map<String, dynamic> dataMap;
if (data is String) {
dataMap = json.decode(data);
} else if (data is Map) {
dataMap = Map<String, dynamic>.from(data);
} else {
return;
}
final bool isTyping = dataMap['is_typing'] == true;
setState(() {
_isOtherUserTyping = isTyping;
});
// Auto-clear typing status after 5 seconds of no update
_typingTimeoutTimer?.cancel();
if (_isOtherUserTyping) {
_typingTimeoutTimer = Timer(const Duration(seconds: 5), () {
if (mounted) {
setState(() {
_isOtherUserTyping = false;
});
}
});
}
} catch (e) {
debugPrint("❌ Error parsing typing data: $e");
}
}
void scrollToBottom({bool animated = false}) {
// In a reversed list, bottom is pixels: 0
if (!scrollController.hasClients) return;
if (animated) {
scrollController.animateTo(
0,
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutCubic,
);
} else {
scrollController.jumpTo(0);
}
}
// ⭐ FIXED: Use stable key that doesn't change with pagination
bool scrollToMessage(String messageKey) {
final key = _messageKeys[messageKey];
if (key != null && key.currentContext != null) {
Scrollable.ensureVisible(
key.currentContext!,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
alignment: 0.3,
);
// Trigger highlight animation
if (key is GlobalKey<SwipeableMessageBubbleState>) {
key.currentState?.highlight();
} else {
final state = key.currentContext!
.findAncestorStateOfType<SwipeableMessageBubbleState>();
state?.highlight();
}
return true;
}
return false;
}
// ⭐ NEW: Search and scroll to message that might not be loaded yet
Future<void> _scrollToMissingMessage(int tagId) async {
final notifier = ref.read(chatMessagesProvider(widget.chatid).notifier);
// Check if it exists in locally loaded messages first
final currentMessages =
ref.read(chatMessagesProvider(widget.chatid)).value ?? [];
final existingIndex = currentMessages.indexWhere((m) => m.id == tagId);
if (existingIndex != -1) {
final targetMsg = currentMessages[existingIndex];
final keyString = _getMessageKeyString(targetMsg);
// 1. Try direct scroll
if (scrollToMessage(keyString)) return;
// 2. If not rendered, perform a smooth animation to force it
final reversedIndex = (currentMessages.length - 1 - existingIndex);
final targetOffset = reversedIndex * 150.0; // Estimated position
await scrollController.animateTo(
targetOffset,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
await Future.delayed(const Duration(milliseconds: 100));
scrollToMessage(keyString);
return;
}
// --- If NOT in memory, paginate silently (no full-screen loader) ---
try {
bool found = false;
int maxRetries = 20;
while (!found && maxRetries > 0) {
final messages =
ref.read(chatMessagesProvider(widget.chatid)).value ?? [];
final index = messages.indexWhere((m) => m.id == tagId);
if (index != -1) {
final targetMsg = messages[index];
final keyString = _getMessageKeyString(targetMsg);
final reversedIndex = (messages.length - 1 - index);
final targetOffset = reversedIndex * 150.0;
await scrollController.animateTo(
targetOffset,
duration: const Duration(milliseconds: 400),
curve: Curves.easeOut,
);
// Retry highlight after animation
for (int retry = 0; retry < 3; retry++) {
await Future.delayed(Duration(milliseconds: 100 + (retry * 100)));
if (scrollToMessage(keyString)) {
found = true;
break;
}
}
if (found) break;
}
if (!notifier.hasNextPage) break;
await notifier.loadMessages();
maxRetries--;
}
if (!found && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Could not find the original message")),
);
}
} catch (e) {
debugPrint("❌ Error searching for message: $e");
}
}
// ⭐ FIXED: Generate unique string key for each message
GlobalKey<SwipeableMessageBubbleState> _getKeyForMessage(String messageKey) {
if (!_messageKeys.containsKey(messageKey)) {
_messageKeys[messageKey] = GlobalKey<SwipeableMessageBubbleState>();
}
return _messageKeys[messageKey] as GlobalKey<SwipeableMessageBubbleState>;
}
// ⭐ FIXED: Generate stable key from message data
String _getMessageKeyString(MessageModel msg) {
// Unique features: ID, type, and creation time
final id = msg.id;
final createdAt = msg.createdAt ?? '';
final message =
msg.message.hashCode; // Add message hash for extra uniqueness
return 'msg_${id}_${createdAt}_$message';
}
String formatMessageDate(DateTime date) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final msgDay = DateTime(date.year, date.month, date.day);
if (msgDay == today) return "Today";
if (msgDay == today.subtract(const Duration(days: 1))) return "Yesterday";
return "${date.day}-${date.month}-${date.year}";
}
String formatTime(DateTime time) {
final hour = time.hour % 12 == 0 ? 12 : time.hour % 12;
final minute = time.minute.toString().padLeft(2, '0');
final period = time.hour >= 12 ? "PM" : "AM";
return "$hour:$minute $period";
}
Widget buildTick(MessageModel message) {
// ⭐ Status 0/1/pending
final isRead = message.isRead;
final isDelivered = message.isDelivered;
// Check if it's an optimistic pending message (huge ID)
if (message.id > 1000000000000) {
return const Icon(Icons.schedule, size: 14, color: Colors.grey);
}
if (isRead == 1) {
return const Icon(Icons.done_all, size: 16, color: Colors.blue);
} else if (isDelivered == 1) {
return const Icon(Icons.done_all, size: 16, color: Colors.grey);
} else {
return const Icon(Icons.check, size: 16, color: Colors.grey);
}
}
// 🖼️ NEW: Show Image Grid Dialog
void _showImageGridDialog(List<String> imageUrls, int initialIndex) {
showDialog(
context: context,
barrierColor: Colors.black87,
builder: (context) =>
ImageGridViewer(imageUrls: imageUrls, initialIndex: initialIndex),
);
}
@override
Widget build(BuildContext context) {
super.build(context);
final chatAsync = ref.watch(chatMessagesProvider(widget.chatid));
final taggedMessage = ref.watch(taggedMessageProvider);
return Scaffold(
appBar: AppBar(
title: Text(
(widget.fileid != null &&
widget.fileid.toString().isNotEmpty &&
widget.fileid.toString() != "0")
? 'File ID : ${widget.fileid.toString()}'
: "Live Chat",
style: const TextStyle(
fontFamily: "Gilroy",
fontWeight: FontWeight.w600,
),
),
backgroundColor: Colors.white,
elevation: 1,
foregroundColor: Colors.black,
centerTitle: true,
actions: [
Padding(
padding: const EdgeInsets.only(right: 12),
child: GestureDetector(
onTap: () {
Get.to(ChatProfileScreen(chatid: widget.chatid));
},
child: Container(
width: 37.446807861328125,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFFFAFAFA),
borderRadius: BorderRadius.circular(6.81),
border: Border.all(
color: const Color(0xFF1E780C),
width: 0.85,
),
),
child: const Icon(Icons.person, color: Color(0xFF1E780C)),
),
),
),
],
),
body: Stack(
children: [
Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(AppAssets.backgroundimages),
fit: BoxFit.cover,
),
),
),
Column(
children: [
Expanded(
child: chatAsync.when(
loading: () => const Center(child: SizedBox.shrink()),
error: (e, _) => Center(child: Text(e.toString())),
data: (messages) {
final seenKeys = <String>{};
final uniqueMessages = messages.where((msg) {
final key = _getMessageKeyString(msg);
if (seenKeys.contains(key)) return false;
seenKeys.add(key);
return true;
}).toList();
final reversedMessages = uniqueMessages.reversed.toList();
return Stack(
children: [
ListView.builder(
controller: scrollController,
reverse: true, // ⭐ Index 0 is now at the bottom
physics: const ClampingScrollPhysics(),
cacheExtent:
1000, // ⭐ Help with deep scrolling visibility
padding: const EdgeInsets.only(
left: 15,
right: 15,
top: 15,
bottom: 15,
),
itemCount: reversedMessages.length,
itemBuilder: (context, index) {
final msg = reversedMessages[index];
final msgDate = DateTime.parse(msg.createdAt!);
// In reversed list, header shows if previous message (index+1) has different date
final showDateHeader =
index == reversedMessages.length - 1 ||
formatMessageDate(
DateTime.parse(
reversedMessages[index + 1].createdAt!,
),
) !=
formatMessageDate(msgDate);
// ⭐ FIXED: Use stable key (no index dependency)
final messageKeyString = _getMessageKeyString(msg);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (showDateHeader)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 10,
),
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 5,
),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.06),
borderRadius: BorderRadius.circular(
12,
),
),
child: Text(
formatMessageDate(msgDate),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
),
),
SwipeableMessageBubble(
message: msg,
msgDate: msgDate,
formatTime: formatTime,
buildTick: buildTick,
onSwipe: () {
ref
.read(
taggedMessageProvider.notifier,
)
.state =
msg;
},
onParentTagTap: (tagId) {
if (tagId == null) return;
// ALWAYS call _scrollToMissingMessage now as it handles both
// local smooth scroll and remote paginate search correctly.
_scrollToMissingMessage(tagId);
},
// 🖼️ NEW: Pass image grid callback
onImageTap: (imageUrls, initialIndex) {
_showImageGridDialog(
imageUrls,
initialIndex,
);
},
key: _getKeyForMessage(messageKeyString),
),
],
);
},
),
// ⭐⭐⭐ Subtle loading indicator at top
if (_isPaginationLoading)
Positioned(
top: 5,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.9),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: const [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
SizedBox(width: 8),
Text(
"Loading...",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
],
);
},
),
),
if (taggedMessage != null)
GestureDetector(
onTap: () {
if (taggedMessage != null) {
// ⭐ FIXED: Need to find the message in the list to get proper key
final chatAsync = ref.read(
chatMessagesProvider(widget.chatid),
);
chatAsync.whenData((messages) {
_scrollToMissingMessage(taggedMessage.id);
});
}
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.grey[200],
border: Border(top: BorderSide(color: Colors.grey[300]!)),
),
child: Row(
children: [
Container(
width: 4,
height: 50,
decoration: BoxDecoration(
color: const Color(0xFF630A73),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
taggedMessage.user?.name ?? "Unknown",
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 13,
color: Color(0xFF630A73),
),
),
const SizedBox(height: 2),
if (taggedMessage.type == "file" &&
taggedMessage.fileName != null)
Row(
children: [
Icon(
Icons.attach_file,
size: 14,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Expanded(
child: Text(
taggedMessage.fileName!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: Colors.grey[700],
),
),
),
],
)
else
Text(
taggedMessage.message,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: Colors.grey[700],
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close, size: 20),
padding: EdgeInsets.zero,
onPressed: () {
ref.read(taggedMessageProvider.notifier).state =
null;
},
),
],
),
),
),
if (_isOtherUserTyping)
Padding(
padding: const EdgeInsets.only(left: 20, bottom: 5),
child: Row(
children: [
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFF1E780C),
),
),
const SizedBox(width: 8),
Text(
"Typing...",
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
],
),
),
ChatInputBox(
widget.chatid,
onMessageSent: () {
setState(() {
userIsReading = false;
});
scrollToBottom(animated: true);
},
),
],
),
if (showScrollButton)
Positioned(
bottom: 150,
right: 20,
child: FloatingActionButton(
heroTag: "scrollBtn",
backgroundColor: Colors.green,
mini: true,
onPressed: () {
scrollToBottom(animated: true);
},
child: const Icon(Icons.arrow_downward, color: Colors.white),
),
),
],
),
);
}
}
// 🖼️ NEW: Image Grid Viewer Widget
class ImageGridViewer extends StatefulWidget {
final List<String> imageUrls;
final int initialIndex;
const ImageGridViewer({
Key? key,
required this.imageUrls,
required this.initialIndex,
}) : super(key: key);
@override
State<ImageGridViewer> createState() => _ImageGridViewerState();
}
class _ImageGridViewerState extends State<ImageGridViewer> {
late PageController _pageController;
late int _currentIndex;
@override
void initState() {
super.initState();
_currentIndex = widget.initialIndex;
_pageController = PageController(initialPage: widget.initialIndex);
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.zero,
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black87,
child: Stack(
children: [
// Main Image Viewer
PageView.builder(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
itemCount: widget.imageUrls.length,
itemBuilder: (context, index) {
return Center(
child: InteractiveViewer(
minScale: 0.5,
maxScale: 4.0,
child: Image.network(
widget.imageUrls[index],
fit: BoxFit.contain,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
color: Colors.white,
),
);
},
errorBuilder: (context, error, stackTrace) {
return const Center(
child: Icon(
Icons.broken_image,
color: Colors.white54,
size: 64,
),
);
},
),
),
);
},
),
// Close Button
Positioned(
top: 40,
right: 20,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 30),
onPressed: () => Navigator.pop(context),
),
),
// Image Counter
Positioned(
top: 50,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${_currentIndex + 1} / ${widget.imageUrls.length}',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
// Thumbnail Grid at Bottom
Positioned(
bottom: 20,
left: 0,
right: 0,
child: Container(
height: 80,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: widget.imageUrls.length,
itemBuilder: (context, index) {
final isSelected = index == _currentIndex;
return GestureDetector(
onTap: () {
_pageController.animateToPage(
index,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
child: Container(
width: 112.8125,
height: 66.88169860839844,
margin: const EdgeInsets.only(right: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? Colors.white
: Colors.transparent,
width: 3,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.network(
widget.imageUrls[index],
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
color: Colors.grey[800],
child: const Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white54,
),
),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[800],
child: const Icon(
Icons.broken_image,
color: Colors.white54,
size: 24,
),
);
},
),
),
),
);
},
),
),
),
],
),
),
);
}
}