import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import '../components/chat_bubble.dart'; import '../components/my_textfield.dart'; import '../constants.dart'; import '../services/auth/auth_service.dart'; import '../services/chat/chat_service.dart'; class ChatPage extends StatefulWidget { final String receiverEmail; final String receiverID; final String chatTitle; const ChatPage({ super.key, required this.receiverEmail, required this.receiverID, required this.chatTitle, }); @override State createState() => _ChatPageState(); } class _ChatPageState extends State { final TextEditingController _messageController = TextEditingController(); final ChatService _chatService = ChatService(); final AuthService _authService = AuthService(); // for textfield focus FocusNode myFocusNode = FocusNode(); @override void initState() { super.initState(); // add listener to focus node myFocusNode.addListener(() { if (myFocusNode.hasFocus) { // cause a delay so that the keyboard has time to show up // then the amount of remaining space will be calculated // then scroll down Future.delayed( const Duration(milliseconds: 500), () => scrollDown(), ); } }); // wait a bit for listview to be built, then scroll to bottom Future.delayed( const Duration(milliseconds: 500), () => scrollDown(), ); } @override void dispose() { myFocusNode.dispose(); _messageController.dispose(); super.dispose(); } // scroll controller final ScrollController _scrollController = ScrollController(); void scrollDown() { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(seconds: 1), curve: Curves.fastOutSlowIn, ); } void sendMessage() async { // send message if not empty if (_messageController.text.isNotEmpty) { await _chatService.sendMessage( widget.receiverID, _messageController.text, ); // clear text controller _messageController.clear(); } scrollDown(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(widget.chatTitle)), body: Column( children: [ // display all messages Expanded(child: _buildMessageList()), // user input _buildUserInput(), ], ), ); } Widget _buildMessageList() { String senderID = _authService.getCurrentUser()!.uid; return StreamBuilder( stream: _chatService.getMessages(senderID, widget.receiverID), builder: (context, snapshot) { // errors if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } // loading if (snapshot.connectionState == ConnectionState.waiting) { return const Text('Loading..'); } // return list view return ListView( controller: _scrollController, children: snapshot.data!.docs.map((doc) => _buildMessageItem(doc)).toList(), ); }, ); } Widget _buildMessageItem(DocumentSnapshot doc) { Map data = doc.data() as Map; // align message to the right if sender is current user, otherwise left bool isCurrentUser = data[Constants.dbFieldMessageSenderId] == _authService.getCurrentUser()!.uid; var alignment = isCurrentUser ? Alignment.centerRight : Alignment.centerLeft; List msgDate = (data[Constants.dbFieldMessageTimestamp] as Timestamp) .toDate() .toIso8601String() .split("T"); return Container( alignment: alignment, child: Column( crossAxisAlignment: isCurrentUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 25.0), child: Text( '${msgDate[0]} ${msgDate[1].substring(0, 5)}', style: const TextStyle(color: Colors.grey, fontSize: 10), ), ), ChatBubble( message: data[Constants.dbFieldMessageText], isCurrentUser: isCurrentUser, ), ], ), ); } Widget _buildUserInput() { return Padding( padding: const EdgeInsets.only(bottom: 10.0), child: Row( children: [ // text should take up most of space Expanded( child: MyTextField( controller: _messageController, hintText: 'Type a message', obscureText: false, focusNode: myFocusNode, ), ), // send button Container( decoration: const BoxDecoration( color: Colors.green, shape: BoxShape.circle, ), margin: const EdgeInsets.only(right: 25), child: IconButton( onPressed: sendMessage, icon: const Icon(Icons.send), color: Colors.white, ), ) ], ), ); } }