feat: standard error handling, web bug fixes, and mock data setup
This commit is contained in:
@@ -1,12 +1,30 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import 'package:doormile/core/theme/app_colors.dart';
|
||||
import 'package:doormile/core/theme/app_theme.dart';
|
||||
import 'package:doormile/core/widgets/common.dart';
|
||||
|
||||
enum _ConnStatus { connecting, connected, disconnected }
|
||||
|
||||
class _ChatMessage {
|
||||
final String text;
|
||||
final bool fromCustomer;
|
||||
_ChatMessage(this.text, {required this.fromCustomer});
|
||||
}
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final String riderName;
|
||||
const ChatScreen({super.key, required this.riderName});
|
||||
final String bookingId;
|
||||
final String? authToken;
|
||||
|
||||
const ChatScreen({
|
||||
super.key,
|
||||
required this.riderName,
|
||||
required this.bookingId,
|
||||
this.authToken,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
@@ -14,25 +32,117 @@ class ChatScreen extends StatefulWidget {
|
||||
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
final TextEditingController _messageController = TextEditingController();
|
||||
final List<String> _messages = [
|
||||
'Hi! I am picking up your parcel now.',
|
||||
'Please let me know if there are any specific instructions.',
|
||||
];
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final List<_ChatMessage> _messages = [];
|
||||
|
||||
WebSocketChannel? _channel;
|
||||
_ConnStatus _status = _ConnStatus.connecting;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_connectWebSocket();
|
||||
}
|
||||
|
||||
void _connectWebSocket() {
|
||||
if (widget.bookingId.isEmpty) {
|
||||
setState(() => _status = _ConnStatus.disconnected);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _status = _ConnStatus.connecting);
|
||||
|
||||
try {
|
||||
String wsUrl = 'ws://api.doormile.com/ws/bookings/${widget.bookingId}/chat?role=customer';
|
||||
if (widget.authToken != null && widget.authToken!.isNotEmpty) {
|
||||
wsUrl += '&token=${widget.authToken}';
|
||||
}
|
||||
|
||||
_channel = WebSocketChannel.connect(
|
||||
Uri.parse(wsUrl),
|
||||
);
|
||||
|
||||
// Trigger connected status on first successful frame after connect
|
||||
_channel!.ready.then((_) {
|
||||
if (mounted) setState(() => _status = _ConnStatus.connected);
|
||||
}).catchError((e) {
|
||||
debugPrint('WS ready error: $e');
|
||||
if (mounted) setState(() => _status = _ConnStatus.disconnected);
|
||||
});
|
||||
|
||||
_channel!.stream.listen(
|
||||
_onMessage,
|
||||
onError: (e) {
|
||||
debugPrint('WS error: $e');
|
||||
if (mounted) setState(() => _status = _ConnStatus.disconnected);
|
||||
},
|
||||
onDone: () {
|
||||
if (mounted) setState(() => _status = _ConnStatus.disconnected);
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('WS connect error: $e');
|
||||
if (mounted) setState(() => _status = _ConnStatus.disconnected);
|
||||
}
|
||||
}
|
||||
|
||||
void _onMessage(dynamic raw) {
|
||||
try {
|
||||
final data = jsonDecode(raw as String) as Map<String, dynamic>;
|
||||
final text = data['text'] as String? ?? '';
|
||||
final sender = data['sender'] as String? ?? '';
|
||||
if (text.isEmpty) return;
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_messages.add(_ChatMessage(text, fromCustomer: sender == 'customer'));
|
||||
});
|
||||
_scrollToBottom();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('WS parse error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _sendMessage() {
|
||||
final text = _messageController.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
|
||||
final payload = jsonEncode({'text': text, 'sender': 'customer'});
|
||||
|
||||
if (_channel != null && _status == _ConnStatus.connected) {
|
||||
_channel!.sink.add(payload);
|
||||
}
|
||||
|
||||
// Optimistically show the message regardless of WS state
|
||||
setState(() {
|
||||
_messages.add(_ChatMessage(text, fromCustomer: true));
|
||||
_messageController.clear();
|
||||
});
|
||||
_scrollToBottom();
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_messageController.dispose();
|
||||
_scrollController.dispose();
|
||||
_channel?.sink.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _sendMessage() {
|
||||
if (_messageController.text.trim().isEmpty) return;
|
||||
setState(() {
|
||||
_messages.add(_messageController.text.trim());
|
||||
_messageController.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -43,43 +153,106 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.riderName, style: AppTheme.headlineSm.copyWith(color: Colors.white, fontSize: 18)),
|
||||
Text('Doormile EV Rider', style: AppTheme.caption.copyWith(color: Colors.white70)),
|
||||
Text(widget.riderName,
|
||||
style: AppTheme.headlineSm.copyWith(color: Colors.white, fontSize: 18)),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: switch (_status) {
|
||||
_ConnStatus.connected => const Color(0xFF4CAF50),
|
||||
_ConnStatus.connecting => const Color(0xFFFF9800),
|
||||
_ConnStatus.disconnected => const Color(0xFFEF5350),
|
||||
},
|
||||
),
|
||||
),
|
||||
Text(
|
||||
switch (_status) {
|
||||
_ConnStatus.connected => 'Connected',
|
||||
_ConnStatus.connecting => 'Connecting...',
|
||||
_ConnStatus.disconnected => 'Disconnected',
|
||||
},
|
||||
style: AppTheme.caption.copyWith(color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (_status == _ConnStatus.disconnected)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
tooltip: 'Reconnect',
|
||||
onPressed: () {
|
||||
_channel?.sink.close();
|
||||
_connectWebSocket();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isRider = index < 2; // Mocking first 2 messages from rider
|
||||
return Align(
|
||||
alignment: isRider ? Alignment.centerLeft : Alignment.centerRight,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: isRider ? AppColors.surfaceContainerHigh : AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(16).copyWith(
|
||||
bottomLeft: isRider ? Radius.zero : const Radius.circular(16),
|
||||
bottomRight: !isRider ? Radius.zero : const Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: _messages.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
_messages[index],
|
||||
style: AppTheme.bodyMd.copyWith(color: isRider ? AppColors.onSurface : Colors.white),
|
||||
_status == _ConnStatus.connecting
|
||||
? 'Connecting to chat...'
|
||||
: 'No messages yet. Say hi!',
|
||||
style: AppTheme.bodyMd.copyWith(color: AppColors.onSurfaceVariant),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final msg = _messages[index];
|
||||
return Align(
|
||||
alignment: msg.fromCustomer
|
||||
? Alignment.centerRight
|
||||
: Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: msg.fromCustomer
|
||||
? AppColors.primary
|
||||
: AppColors.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(16).copyWith(
|
||||
bottomLeft: msg.fromCustomer
|
||||
? const Radius.circular(16)
|
||||
: Radius.zero,
|
||||
bottomRight: msg.fromCustomer
|
||||
? Radius.zero
|
||||
: const Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
msg.text,
|
||||
style: AppTheme.bodyMd.copyWith(
|
||||
color: msg.fromCustomer
|
||||
? Colors.white
|
||||
: AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16).copyWith(bottom: MediaQuery.of(context).padding.bottom + 16),
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: AppColors.surfaceContainer)),
|
||||
@@ -89,6 +262,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _messageController,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Message ${widget.riderName}...',
|
||||
border: OutlineInputBorder(
|
||||
@@ -97,7 +272,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColors.surfaceContainerLowest,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user