From d8ea50b94b5e26e0562bf4e10246f505e1baef27 Mon Sep 17 00:00:00 2001 From: rudramistry001 Date: Wed, 7 Feb 2024 16:38:45 +0530 Subject: [PATCH] message send recieve --- login/lib/api/apis.dart | 49 +++- login/lib/dialogs/my_date_util.dart | 10 + login/lib/model/messages.dart | 39 +++ login/lib/screens/chat_screen.dart | 320 ++++++++++----------- login/lib/screens/splashscreen.dart | 2 +- login/lib/screens/user_chat_screen.dart | 41 ++- login/lib/screens/view_profile_screen.dart | 101 +++++++ login/lib/widgets/message.dart | 5 - login/lib/widgets/message_card.dart | 155 ++++++++++ 9 files changed, 540 insertions(+), 182 deletions(-) create mode 100644 login/lib/dialogs/my_date_util.dart create mode 100644 login/lib/model/messages.dart create mode 100644 login/lib/screens/view_profile_screen.dart delete mode 100644 login/lib/widgets/message.dart diff --git a/login/lib/api/apis.dart b/login/lib/api/apis.dart index 134917d..20f3773 100644 --- a/login/lib/api/apis.dart +++ b/login/lib/api/apis.dart @@ -1,9 +1,12 @@ +// ignore_for_file: avoid_print + import 'dart:io'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:login/model/chat_user_model.dart'; +import 'package:login/model/messages.dart'; class APIs { // for authentication @@ -73,7 +76,6 @@ class APIs { }); } - //for updating the profile picture // update profile picture of user static Future updateProfilePicture(File file) async { //getting image file extension @@ -98,11 +100,50 @@ class APIs { .update({'image': me.Image}); } - // CHAT SCREEN RELATED APIS + ///************** Chat Screen Related APIs ************** + + // chats (collection) --> conversation_id (doc) --> messages (collection) --> message (doc) + + // useful for getting conversation id + static String getConversationID(String id) => user.uid.hashCode <= id.hashCode + ? '${user.uid}_$id' + : '${id}_${user.uid}'; + static Stream>> getAllMessages( ChatUser user) { - return firestore.collection('messages').snapshots(); + return firestore + .collection('chats/${getConversationID(user.Id)}/messages/') + .orderBy('sent', descending: false) + .snapshots(); } - static getUserInfo(ChatUser user) {} + // for sending message + static Future sendMessage( + ChatUser chatUser, + String msg, + ) async { + //message sending time (also used as id) + final time = DateTime.now().millisecondsSinceEpoch.toString(); + + //message to send + final Message message = Message( + toId: chatUser.Id, + msg: msg, + read: '', + type: Type.text, + fromId: user.uid, + sent: time); + + final ref = firestore + .collection('chats/${getConversationID(chatUser.Id)}/messages/'); + await ref.doc(time).set(message.toJson()); + } + + //update read status of message + static Future updateMessageReadStatus(Message message) async { + firestore + .collection('chats/${getConversationID(message.fromId)}/messages/') + .doc(message.sent) + .update({'read': DateTime.now().millisecondsSinceEpoch.toString()}); + } } diff --git a/login/lib/dialogs/my_date_util.dart b/login/lib/dialogs/my_date_util.dart new file mode 100644 index 0000000..9f47887 --- /dev/null +++ b/login/lib/dialogs/my_date_util.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class MyDateUtil { + // for getting formatted time from milliSecondsSinceEpochs String + static String getFormattedTime( + {required BuildContext context, required String time}) { + final date = DateTime.fromMillisecondsSinceEpoch(int.parse(time)); + return TimeOfDay.fromDateTime(date).format(context); + } +} diff --git a/login/lib/model/messages.dart b/login/lib/model/messages.dart new file mode 100644 index 0000000..441754b --- /dev/null +++ b/login/lib/model/messages.dart @@ -0,0 +1,39 @@ +class Message { + Message({ + required this.toId, + required this.msg, + required this.read, + required this.type, + required this.fromId, + required this.sent, + }); + + late final String toId; + late final String msg; + late final String read; + late final String fromId; + late final String sent; + late final Type type; + + Message.fromJson(Map json) { + toId = json['toId'].toString(); + msg = json['msg'].toString(); + read = json['read'].toString(); + type = json['type'].toString() == Type.image.name ? Type.image : Type.text; + fromId = json['fromId'].toString(); + sent = json['sent'].toString(); + } + + Map toJson() { + final data = {}; + data['toId'] = toId; + data['msg'] = msg; + data['read'] = read; + data['type'] = type.name; + data['fromId'] = fromId; + data['sent'] = sent; + return data; + } +} + +enum Type { text, image } diff --git a/login/lib/screens/chat_screen.dart b/login/lib/screens/chat_screen.dart index 91380b6..ef06dd4 100644 --- a/login/lib/screens/chat_screen.dart +++ b/login/lib/screens/chat_screen.dart @@ -1,165 +1,165 @@ -import 'dart:convert'; -import 'package:chat_bubbles/bubbles/bubble_normal.dart'; -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:login/widgets/message.dart'; +// import 'dart:convert'; +// import 'package:chat_bubbles/bubbles/bubble_normal.dart'; +// import 'package:flutter/material.dart'; +// import 'package:http/http.dart' as http; +// import 'package:login/widgets/message.dart'; -class ChatScreen extends StatefulWidget { - const ChatScreen({super.key}); +// class ChatScreen extends StatefulWidget { +// const ChatScreen({super.key}); - @override - State createState() => _ChatScreenState(); -} +// @override +// State createState() => _ChatScreenState(); +// } -class _ChatScreenState extends State { - TextEditingController controller = TextEditingController(); - ScrollController scrollController = ScrollController(); - List msgs = []; - bool isTyping = false; +// class _ChatScreenState extends State { +// TextEditingController controller = TextEditingController(); +// ScrollController scrollController = ScrollController(); +// List msgs = []; +// bool isTyping = false; - void sendMsg() async { - String text = controller.text; - String apiKey = "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"; - controller.clear(); - try { - if (text.isNotEmpty) { - setState(() { - msgs.insert(0, Message(true, text)); - isTyping = true; - }); - scrollController.animateTo(0.0, - duration: const Duration(seconds: 1), curve: Curves.easeOut); - var response = await http.post( - Uri.parse("https://api.openai.com/v1/chat/completions"), - headers: { - "Authorization": "Bearer $apiKey", - "Content-Type": "application/json" - }, - body: jsonEncode({ - "model": "gpt-3.5-turbo", - "messages": [ - {"role": "user", "content": text} - ] - })); - if (response.statusCode == 200) { - var json = jsonDecode(response.body); - setState(() { - isTyping = false; - msgs.insert( - 0, - Message( - false, - json["choices"][0]["message"]["content"] - .toString() - .trimLeft())); - }); - scrollController.animateTo(0.0, - duration: const Duration(seconds: 1), curve: Curves.easeOut); - } - } - } on Exception { - // ignore: use_build_context_synchronously - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text("Some error occurred, please try again!"))); - } - } +// void sendMsg() async { +// String text = controller.text; +// String apiKey = "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"; +// controller.clear(); +// try { +// if (text.isNotEmpty) { +// setState(() { +// msgs.insert(0, Message(true, text)); +// isTyping = true; +// }); +// scrollController.animateTo(0.0, +// duration: const Duration(seconds: 1), curve: Curves.easeOut); +// var response = await http.post( +// Uri.parse("https://api.openai.com/v1/chat/completions"), +// headers: { +// "Authorization": "Bearer $apiKey", +// "Content-Type": "application/json" +// }, +// body: jsonEncode({ +// "model": "gpt-3.5-turbo", +// "messages": [ +// {"role": "user", "content": text} +// ] +// })); +// if (response.statusCode == 200) { +// var json = jsonDecode(response.body); +// setState(() { +// isTyping = false; +// msgs.insert( +// 0, +// Message( +// false, +// json["choices"][0]["message"]["content"] +// .toString() +// .trimLeft())); +// }); +// scrollController.animateTo(0.0, +// duration: const Duration(seconds: 1), curve: Curves.easeOut); +// } +// } +// } on Exception { +// // ignore: use_build_context_synchronously +// ScaffoldMessenger.of(context).showSnackBar(const SnackBar( +// content: Text("Some error occurred, please try again!"))); +// } +// } - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text("Chat Bot"), - ), - body: Column( - children: [ - const SizedBox( - height: 8, - ), - Expanded( - child: ListView.builder( - controller: scrollController, - itemCount: msgs.length, - shrinkWrap: true, - reverse: true, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: isTyping && index == 0 - ? Column( - children: [ - BubbleNormal( - text: msgs[0].msg, - isSender: true, - color: Colors.blue.shade100, - ), - const Padding( - padding: EdgeInsets.only(left: 16, top: 4), - child: Align( - alignment: Alignment.centerLeft, - child: Text("Typing...")), - ) - ], - ) - : BubbleNormal( - text: msgs[index].msg, - isSender: msgs[index].isSender, - color: msgs[index].isSender - ? Colors.blue.shade100 - : Colors.grey.shade200, - )); - }), - ), - Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - width: double.infinity, - height: 40, - decoration: BoxDecoration( - color: Colors.grey[200], - borderRadius: BorderRadius.circular(10)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: TextField( - controller: controller, - textCapitalization: TextCapitalization.sentences, - onSubmitted: (value) { - sendMsg(); - }, - textInputAction: TextInputAction.send, - showCursor: true, - decoration: const InputDecoration( - border: InputBorder.none, hintText: "Enter text"), - ), - ), - ), - ), - ), - InkWell( - onTap: () { - sendMsg(); - }, - child: Container( - height: 40, - width: 40, - decoration: BoxDecoration( - color: Colors.blue, - borderRadius: BorderRadius.circular(30)), - child: const Icon( - Icons.send, - color: Colors.white, - ), - ), - ), - const SizedBox( - width: 8, - ) - ], - ), - ], - ), - ); - } -} +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: AppBar( +// title: const Text("Chat Bot"), +// ), +// body: Column( +// children: [ +// const SizedBox( +// height: 8, +// ), +// Expanded( +// child: ListView.builder( +// controller: scrollController, +// itemCount: msgs.length, +// shrinkWrap: true, +// reverse: true, +// itemBuilder: (context, index) { +// return Padding( +// padding: const EdgeInsets.symmetric(vertical: 4), +// child: isTyping && index == 0 +// ? Column( +// children: [ +// BubbleNormal( +// text: msgs[0].msg, +// isSender: true, +// color: Colors.blue.shade100, +// ), +// const Padding( +// padding: EdgeInsets.only(left: 16, top: 4), +// child: Align( +// alignment: Alignment.centerLeft, +// child: Text("Typing...")), +// ) +// ], +// ) +// : BubbleNormal( +// text: msgs[index].msg, +// isSender: msgs[index].isSender, +// color: msgs[index].isSender +// ? Colors.blue.shade100 +// : Colors.grey.shade200, +// )); +// }), +// ), +// Row( +// children: [ +// Expanded( +// child: Padding( +// padding: const EdgeInsets.all(8.0), +// child: Container( +// width: double.infinity, +// height: 40, +// decoration: BoxDecoration( +// color: Colors.grey[200], +// borderRadius: BorderRadius.circular(10)), +// child: Padding( +// padding: const EdgeInsets.symmetric(horizontal: 8), +// child: TextField( +// controller: controller, +// textCapitalization: TextCapitalization.sentences, +// onSubmitted: (value) { +// sendMsg(); +// }, +// textInputAction: TextInputAction.send, +// showCursor: true, +// decoration: const InputDecoration( +// border: InputBorder.none, hintText: "Enter text"), +// ), +// ), +// ), +// ), +// ), +// InkWell( +// onTap: () { +// sendMsg(); +// }, +// child: Container( +// height: 40, +// width: 40, +// decoration: BoxDecoration( +// color: Colors.blue, +// borderRadius: BorderRadius.circular(30)), +// child: const Icon( +// Icons.send, +// color: Colors.white, +// ), +// ), +// ), +// const SizedBox( +// width: 8, +// ) +// ], +// ), +// ], +// ), +// ); +// } +// } diff --git a/login/lib/screens/splashscreen.dart b/login/lib/screens/splashscreen.dart index 75506ae..2cc09f9 100644 --- a/login/lib/screens/splashscreen.dart +++ b/login/lib/screens/splashscreen.dart @@ -4,7 +4,6 @@ import 'package:login/api/apis.dart'; import 'package:login/auth/loginscreen.dart'; import 'package:login/main.dart'; import 'package:login/screens/homescreen.dart'; -import 'package:login/screens/onboard_screen.dart'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -25,6 +24,7 @@ class _SplashScreenState extends State { statusBarColor: Colors.white)); if (APIs.auth.currentUser != null) { + // ignore: avoid_print print('\nUser: ${APIs.auth.currentUser}'); //navigate to home screen Navigator.pushReplacement( diff --git a/login/lib/screens/user_chat_screen.dart b/login/lib/screens/user_chat_screen.dart index 73a1268..c7f843e 100644 --- a/login/lib/screens/user_chat_screen.dart +++ b/login/lib/screens/user_chat_screen.dart @@ -1,12 +1,15 @@ +// ignore_for_file: avoid_print + import 'package:cached_network_image/cached_network_image.dart'; -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:login/api/apis.dart'; import 'package:login/main.dart'; import 'package:login/model/chat_user_model.dart'; -import 'package:login/screens/profile_screen.dart'; +import 'package:login/model/messages.dart'; +import 'package:login/screens/view_profile_screen.dart'; +import 'package:login/widgets/message_card.dart'; class UserChatScreen extends StatefulWidget { final ChatUser user; @@ -17,6 +20,10 @@ class UserChatScreen extends StatefulWidget { } class _UserChatScreenState extends State { + //for storing all messages + List _list = []; +//for handling message text controlling + final _textController = TextEditingController(); @override Widget build(BuildContext context) { return SafeArea( @@ -28,7 +35,8 @@ class _UserChatScreenState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => ProfileScreen(user: APIs.me)), + builder: (context) => + ViewProfileScreen(user: widget.user)), ); }, child: Row( @@ -73,7 +81,7 @@ class _UserChatScreenState extends State { children: [ Expanded( child: StreamBuilder( - stream: APIs.getAllUsers(), + stream: APIs.getAllMessages(widget.user), builder: (context, snapshot) { switch (snapshot.connectionState) { // if data s loading @@ -86,20 +94,22 @@ class _UserChatScreenState extends State { //if some or all data is loaded then show it case ConnectionState.active: case ConnectionState.done: - // final data = snapshot.data?.docs; - // _list = data - // ?.map((e) => ChatUser.fromJson(e.data())) - // .toList() ?? - // []; + final data = snapshot.data?.docs; + + _list = data + ?.map((e) => Message.fromJson(e.data())) + .toList() ?? + []; - final _list = ['hii', 'hello']; if (_list.isNotEmpty) { return ListView.builder( itemCount: _list.length, // Use the length of the list padding: EdgeInsets.only(top: mq.height * .01), physics: const BouncingScrollPhysics(), itemBuilder: (context, index) { - return Text('Message: ${_list[index]}'); + return MessageCard( + message: _list[index], + ); }, ); } else { @@ -142,6 +152,7 @@ class _UserChatScreenState extends State { ), Expanded( child: TextField( + controller: _textController, keyboardType: TextInputType.multiline, maxLines: null, decoration: InputDecoration( @@ -168,7 +179,13 @@ class _UserChatScreenState extends State { //send message button ), MaterialButton( - onPressed: () {}, + onPressed: () { + if (_textController.text.isNotEmpty) { + //simply send message + APIs.sendMessage(widget.user, _textController.text); + _textController.clear(); + } + }, minWidth: 1, padding: EdgeInsets.only( top: 5.sp, bottom: 5.sp, right: 5.sp, left: 5.sp), diff --git a/login/lib/screens/view_profile_screen.dart b/login/lib/screens/view_profile_screen.dart new file mode 100644 index 0000000..0b14ad2 --- /dev/null +++ b/login/lib/screens/view_profile_screen.dart @@ -0,0 +1,101 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:login/main.dart'; +import 'package:login/model/chat_user_model.dart'; + +//view profile screen -- to view profile of user +class ViewProfileScreen extends StatefulWidget { + final ChatUser user; + + const ViewProfileScreen({super.key, required this.user}); + + @override + State createState() => _ViewProfileScreenState(); +} + +class _ViewProfileScreenState extends State { + @override + Widget build(BuildContext context) { + return GestureDetector( + // for hiding keyboard + onTap: () => FocusScope.of(context).unfocus(), + child: Scaffold( + //app bar + appBar: AppBar(title: Text(widget.user.Name)), + floatingActionButton: //user about + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Joined On: ', + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.w500, + fontSize: 15), + ), + // Text( + // // MyDateUtil.getLastMessageTime( + // // context: context, + // // time: widget.user.createdAt, + // // showYear: true), + // style: const TextStyle(color: Colors.black54, fontSize: 15)), + ], + ), + + //body + body: Padding( + padding: EdgeInsets.symmetric(horizontal: mq.width * .05), + child: SingleChildScrollView( + child: Column( + children: [ + // for adding some space + SizedBox(width: mq.width, height: mq.height * .03), + + //user profile picture + ClipRRect( + borderRadius: BorderRadius.circular(mq.height * .1), + child: CachedNetworkImage( + width: mq.height * .2, + height: mq.height * .2, + fit: BoxFit.cover, + imageUrl: widget.user.Image, + errorWidget: (context, url, error) => const CircleAvatar( + child: Icon(CupertinoIcons.person)), + ), + ), + + // for adding some space + SizedBox(height: mq.height * .03), + + // user email label + Text(widget.user.Email, + style: + const TextStyle(color: Colors.black87, fontSize: 16)), + + // for adding some space + SizedBox(height: mq.height * .02), + + //user about + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'About: ', + style: TextStyle( + color: Colors.black87, + fontWeight: FontWeight.w500, + fontSize: 15), + ), + Text(widget.user.About, + style: const TextStyle( + color: Colors.black54, fontSize: 15)), + ], + ), + ], + ), + ), + )), + ); + } +} diff --git a/login/lib/widgets/message.dart b/login/lib/widgets/message.dart deleted file mode 100644 index e2d7739..0000000 --- a/login/lib/widgets/message.dart +++ /dev/null @@ -1,5 +0,0 @@ -class Message { - bool isSender; - String msg; - Message(this.isSender, this.msg); -} diff --git a/login/lib/widgets/message_card.dart b/login/lib/widgets/message_card.dart index 8b13789..d55e0e7 100644 --- a/login/lib/widgets/message_card.dart +++ b/login/lib/widgets/message_card.dart @@ -1 +1,156 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:login/api/apis.dart'; +import 'package:login/dialogs/my_date_util.dart'; +import 'package:login/main.dart'; +import 'package:login/model/messages.dart'; +class MessageCard extends StatefulWidget { + final Message message; + const MessageCard({super.key, required this.message}); + + @override + State createState() => _MessageCardState(); +} + +class _MessageCardState extends State { + @override + Widget build(BuildContext context) { + return APIs.user.uid == widget.message.fromId + ? _greenMessage() + : _blueMessage(); + } + + // sender or another user message container + + Widget _blueMessage() { + //update last read message if sender and receiver are different + if (widget.message.read.isEmpty) { + APIs.updateMessageReadStatus(widget.message); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + //message content + Flexible( + child: Container( + padding: EdgeInsets.all(widget.message.type == Type.image + ? mq.width * .03 + : mq.width * .04), + margin: EdgeInsets.symmetric( + horizontal: mq.width * .04, vertical: mq.height * .01), + decoration: BoxDecoration( + color: const Color.fromARGB(255, 221, 245, 255), + border: Border.all(color: Colors.lightBlue), + //making borders curved + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + bottomRight: Radius.circular(30))), + child: widget.message.type == Type.text + ? + //show text + Text( + widget.message.msg, + style: const TextStyle(fontSize: 15, color: Colors.black87), + ) + : + //show image + ClipRRect( + borderRadius: BorderRadius.circular(15), + child: CachedNetworkImage( + imageUrl: widget.message.msg, + placeholder: (context, url) => const Padding( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(strokeWidth: 2), + ), + errorWidget: (context, url, error) => + const Icon(Icons.image, size: 70), + ), + ), + ), + ), + + //message time + Padding( + padding: EdgeInsets.only(right: mq.width * .04), + child: Text( + MyDateUtil.getFormattedTime( + context: context, time: widget.message.sent), + style: const TextStyle(fontSize: 13, color: Colors.black54), + ), + ), + ], + ); + } + + Widget _greenMessage() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + //message time + Row( + children: [ + //for adding some space + SizedBox(width: mq.width * .04), + + //double tick blue icon for message read + if (widget.message.read.isNotEmpty) + const Icon(Icons.done_all_rounded, color: Colors.blue, size: 20), + + //for adding some space + const SizedBox(width: 2), + + //sent time + Text( + MyDateUtil.getFormattedTime( + context: context, time: widget.message.sent), + style: const TextStyle(fontSize: 13, color: Colors.black54), + ), + ], + ), + + //message content + Flexible( + child: Container( + padding: EdgeInsets.all(widget.message.type == Type.image + ? mq.width * .03 + : mq.width * .04), + margin: EdgeInsets.symmetric( + horizontal: mq.width * .04, vertical: mq.height * .01), + decoration: BoxDecoration( + color: const Color.fromARGB(255, 218, 255, 176), + border: Border.all(color: Colors.lightGreen), + //making borders curved + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + bottomLeft: Radius.circular(30))), + child: widget.message.type == Type.text + ? + //show text + Text( + widget.message.msg, + style: const TextStyle(fontSize: 15, color: Colors.black87), + ) + : + //show image + ClipRRect( + borderRadius: BorderRadius.circular(15), + child: CachedNetworkImage( + imageUrl: widget.message.msg, + placeholder: (context, url) => const Padding( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(strokeWidth: 2), + ), + errorWidget: (context, url, error) => + const Icon(Icons.image, size: 70), + ), + ), + ), + ), + ], + ); + } +}