From 1ba9e981e174f2f405cc74ccaae8b38c7c631ad0 Mon Sep 17 00:00:00 2001 From: Agboola Onasanya Date: Wed, 26 Mar 2025 13:41:56 +0000 Subject: [PATCH] Messaging Implemented Chat Screens, Profile Pages, Search Pages WIP: User profile, new chat button Intended: Groups, Calls, Voice Messages Properly deleting chats, offline storage of messages --- lib/components/constants.dart | 13 +- lib/components/messaging_provider.dart | 134 +++++++-- lib/components/messaging_routes.dart | 12 +- lib/main.dart | 1 + lib/main_sub_test.dart | 167 ++++++++++++ lib/models/chats.dart | 34 ++- lib/models/conversations.dart | 14 +- lib/models/details.dart | 3 + lib/models/messages.dart | 40 +-- lib/models/users.dart | 8 +- lib/pages/conversations_page.dart | 313 +++++++++++++++++----- lib/pages/home_page.dart | 95 ++++--- lib/pages/login_page.dart | 24 +- lib/pages/profile_page.dart | 2 +- lib/pages/recent_converstations_page.dart | 54 ++-- lib/pages/registeration_page.dart | 62 +++-- lib/pages/search_page.dart | 70 ++++- pubspec.yaml | 2 +- 18 files changed, 812 insertions(+), 236 deletions(-) create mode 100644 lib/main_sub_test.dart diff --git a/lib/components/constants.dart b/lib/components/constants.dart index 49e4664..de320f2 100644 --- a/lib/components/constants.dart +++ b/lib/components/constants.dart @@ -1,14 +1,17 @@ -//const endPoint = 'http://10.15.45.11/v1'; -//const endPoint = 'https://api.theonasanyas.com/v1'; class Constants { - static Constants instance = Constants(); + static Constants instance = Constants(); + + var endPoint = 'https://api.theonasanyas.com/v1'; + var projectId = '67833160000cb0801fdb'; + + // var endPoint = 'https://cloud.appwrite.io/v1'; + // var projectId = '678bdc7c00203eff8af5'; - var endPoint = 'https://cloud.appwrite.io/v1'; - var projectId = '678bdc7c00203eff8af5'; var selfSigned = false; var databaseId = '678c8659000a7d6b5bff'; var userCollectionId = '678c865e002fe1cddd06'; var chatsCollectionId = '678c866c0038822276ad'; var convCollectionId = '678c8667001944dee794'; + var conversationLinkId = '67b949c9001234c0d7b1'; var imageStorageBucket = '678d2162002a6770aaad'; } diff --git a/lib/components/messaging_provider.dart b/lib/components/messaging_provider.dart index fdf3e8a..8508a50 100644 --- a/lib/components/messaging_provider.dart +++ b/lib/components/messaging_provider.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:appwrite/appwrite.dart'; import 'package:appwrite/models.dart' as model; @@ -30,6 +31,7 @@ class MessagingProviders extends ChangeNotifier { late model.User? user; static MessagingProviders instance = MessagingProviders(); + MessagingProviders() { _init(); @@ -99,12 +101,13 @@ class MessagingProviders extends ChangeNotifier { void logout() async { try { + await updateLastSeen(); await account.deleteSession(sessionId: 'current'); user = null; status = AuthStatus.notAuthenticated; // await onSuccess(); await NavigationService.instance.navigateToReplacement("login"); - SnackBarService.instance.showSnackBarError("Logged out Successfully"); + SnackBarService.instance.showSnackBarError("logged out successfully"); } on AppwriteException catch (e) { SnackBarService.instance.showSnackBarError(e.message!); } on Exception catch (e) { @@ -122,8 +125,8 @@ class MessagingProviders extends ChangeNotifier { status = AuthStatus.authenticated; await onSuccess(user!.$id); - SnackBarService.instance.showSnackBarSuccess("Welcome! ${user!.name}"); - await updateLastSeen(); + SnackBarService.instance.showSnackBarSuccess("welcome! ${user!.name}"); + // await updateLastSeen(); NavigationService.instance.goBack(); NavigationService.instance.navigateToReplacement("home"); } on AppwriteException catch (e) { @@ -181,7 +184,8 @@ class MessagingProviders extends ChangeNotifier { ); } - Stream getUser(String userId) { + Stream getUser(String userId) { + // var _userData = database.getDocument( databaseId: Constants.instance.databaseId, collectionId: Constants.instance.userCollectionId, @@ -189,12 +193,14 @@ class MessagingProviders extends ChangeNotifier { ); return _userData.asStream().map((_document) { - return Users.fromData(_document); + return User.fromData(_document); }); } - Stream> getAllUsers(String _searchText) { - List _returnValue = []; + Stream> getAllUsers(String _searchText) async* { + // + // while (true) { + List _returnValue = []; if (_searchText.isNotEmpty) { var _usersData = database.listDocuments( databaseId: Constants.instance.databaseId, @@ -204,23 +210,27 @@ class MessagingProviders extends ChangeNotifier { ], ); - return _usersData.asStream().map( + yield* _usersData.asStream().map( (_documentList) { for (var _document in _documentList.documents) { - _returnValue.add(Users.fromData(_document)); + _returnValue.add(User.fromData(_document)); } + _returnValue.sort((a,b) => b.lastSeen.compareTo(a.lastSeen)); return _returnValue; }, ); } else { - return Stream.empty(); + yield* Stream.empty(); } + // } } - Stream> getUserConversation(String userId) { -// + Stream> getUserConversation(String userId) async* { + // + // while (true) { List _chatId = []; List _details = []; + List _lastModified = []; List _returnValue = []; var _conversationData = database.getDocument( @@ -229,32 +239,117 @@ class MessagingProviders extends ChangeNotifier { documentId: userId.trim(), ); - return _conversationData.asStream().map( + yield* _conversationData.asStream().map( (_snapshot) { _chatId = _snapshot.data['chatId']; _details = _snapshot.data['details']; + _lastModified = _snapshot.data['lastModified']; for (int i = 0; i < _chatId.length; i++) { - _returnValue.add(Conversations.fromData(_chatId[i], _details[i])); + _returnValue.add(Conversations.fromData( + _chatId[i], _details[i], _lastModified[i])); } + _returnValue.sort((a, b) => b.lastModified!.compareTo(a.lastModified!)); return _returnValue; }, ); + // } } - Stream getChat(String _chatId) { + + // Stream> getUserConversations(String userId) async* { + // // + // // while (true) { + // List _chatId = []; + // List _details = []; + // List _lastModified = []; + // List _returnValue = []; + + // var _conversationData = await database.getDocument( + // databaseId: Constants.instance.databaseId, + // collectionId: Constants.instance.convCollectionId, + // documentId: userId.trim(), + // ); + + // yield _conversationData.toMap map( + // (_snapshot) { + // _chatId = _snapshot.data['chatId']; + // _details = _snapshot.data['details']; + // _lastModified = _snapshot.data['lastModified']; + + // for (int i = 0; i < _chatId.length; i++) { + // _returnValue.add(Conversations.fromData( + // _chatId[i], _details[i], _lastModified[i])); + // } + // _returnValue.sort((a, b) => b.lastModified!.compareTo(a.lastModified!)); + // return _returnValue; + // }, + // ); + // // } + // } + + + + Stream getMessage(String _chatId) async* { + // + // while (true) { var _chat = database.getDocument( databaseId: Constants.instance.databaseId, collectionId: Constants.instance.chatsCollectionId, documentId: _chatId, ); - return _chat.asStream().map((_document) { - return Chats.fromData(_document); - }); + yield* _chat.asStream().map( + (_document) { + return Chats.fromDocument(_document); + }, + ); + // } } - uploadUserImage(XFile? file, String _uid) async { + Future getConversationLink(var _linkId) async { + // + try { + var _conversationLinkDetail = await database.getDocument( + databaseId: Constants.instance.databaseId, + collectionId: Constants.instance.conversationLinkId, + documentId: _linkId, + ); + notifyListeners(); + return _conversationLinkDetail.data['chatId']; + } on AppwriteException catch (e) { + notifyListeners(); + return ''; + } + } + + Future newMesaage(Map _chat) async { + // + var chatId = ID.unique(); + await database.createDocument( + databaseId: Constants.instance.databaseId, + collectionId: Constants.instance.chatsCollectionId, + documentId: chatId, + data: _chat, + ); + notifyListeners(); + return chatId; + } + + Future sendMessage( + String chatId, Map _messages) async { + // + await database.updateDocument( + databaseId: Constants.instance.databaseId, + collectionId: Constants.instance.chatsCollectionId, + documentId: chatId, + data: _messages, + ); + notifyListeners(); + } + + Future uploadUserImage(XFile? file, String _uid) async { + // notifyListeners(); try { File _file = File(file!.path); @@ -279,5 +374,6 @@ class MessagingProviders extends ChangeNotifier { // } notifyListeners(); + return ""; } } diff --git a/lib/components/messaging_routes.dart b/lib/components/messaging_routes.dart index fdf7661..9e3b50a 100644 --- a/lib/components/messaging_routes.dart +++ b/lib/components/messaging_routes.dart @@ -7,11 +7,9 @@ class MessagingRoutes { // static MessagingRoutes instance = MessagingRoutes(); - Map get messagingRoutes { - return { - "login": (BuildContext _context) => LoginPage(), - "register": (BuildContext _context) => RegisterationPage(), - "home": (BuildContext _context) => HomePage(), - }; - } + Map get messagingRoutes => { + "login": (BuildContext _context) => LoginPage(), + "register": (BuildContext _context) => RegisterationPage(), + "home": (BuildContext _context) => HomePage(), + }; } diff --git a/lib/main.dart b/lib/main.dart index 197509d..1a850c5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ void main() { } class MessagingApp extends StatelessWidget { + const MessagingApp({super.key}); // This widget is the root of your application. diff --git a/lib/main_sub_test.dart b/lib/main_sub_test.dart new file mode 100644 index 0000000..ac4ea79 --- /dev/null +++ b/lib/main_sub_test.dart @@ -0,0 +1,167 @@ +import 'package:appwrite/appwrite.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'FlAppwrite Realtime Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const HomePage(), + ); + } +} + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + List> items = []; + final TextEditingController _nameController = TextEditingController(); + RealtimeSubscription? subscription; + late final Client client; + final database = '67db1d75002a1826f6f9'; // your database id + final itemsCollection = '67db1e36001ffd2719b7'; // your collection id + late final Databases databases; + + @override + initState() { + super.initState(); + + client = Client() + .setEndpoint('https://api.theonasanyas.com/v1') + .setProject('67833160000cb0801fdb'); // your project id + databases = Databases(client); + loadItems(); + subscribe(); + } + + loadItems() async { + try { + final res = await databases.listDocuments( + databaseId: database, + collectionId: itemsCollection, + ); + setState(() { + items = List>.from(res.documents.map( + (e) => e.data, + )); + }); + } on AppwriteException catch (e) { + print(e.message); + } + } + + void subscribe() { + final realtime = Realtime(client); + + subscription = realtime.subscribe([ + 'documents' // subscribe to all documents in every database and collection + ]); + + // listen to changes + subscription!.stream.listen((data) { + // data will consist of `events` and a `payload` + final event = data.events.first; + if (data.payload.isNotEmpty) { + if (event.endsWith('.create')) { + var item = data.payload; + items.add(item); + setState(() {}); + } else if (event.endsWith('.delete')) { + var item = data.payload; + items.removeWhere((it) => it['\$id'] == item['\$id']); + setState(() {}); + } + } + }); + } + + @override + dispose() { + subscription?.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('FlAppwrite Realtime Demo'), + ), + body: ListView(children: [ + ...items.map((item) => ListTile( + title: Text(item['name']), + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + await databases.deleteDocument( + databaseId: database, + collectionId: itemsCollection, + documentId: item['\$id'], + ); + }, + ), + )), + ]), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () { + // dialog to add new item + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Add new item'), + content: TextField( + controller: _nameController, + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.of(context).pop(), + ), + TextButton( + child: const Text('Add'), + onPressed: () { + // add new item + final name = _nameController.text; + if (name.isNotEmpty) { + _nameController.clear(); + _addItem(name); + } + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + }, + ), + ); + } + + void _addItem(String name) async { + try { + await databases.createDocument( + databaseId: database, + collectionId: itemsCollection, + documentId: ID.unique(), + data: {'name': name}, + ); + } on AppwriteException catch (e) { + print(e.message); + } + } +} diff --git a/lib/models/chats.dart b/lib/models/chats.dart index 9241b8b..c168242 100644 --- a/lib/models/chats.dart +++ b/lib/models/chats.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:appwrite/models.dart'; import 'package:messaging_app/models/messages.dart'; @@ -5,7 +7,7 @@ class Chats { String ownerId; List members; String? type; - List? messages; + List>? messages; Chats({ required this.ownerId, @@ -14,25 +16,29 @@ class Chats { required this.messages, }); - factory Chats.fromData(Document _document) { - List _outMessages = []; - List _outMembers = []; - List _messages = _document.data['messages']; - - for (var _message in _messages) { - _outMessages.add(Messages.fromData(_message)); - } + factory Chats.fromDocument(Document _document) { + // + // + List> _messagesList = []; + List _members = _document.data['members']; + List _messages = _document.data['messages']; - List _members = _document.data['members']; - for (var _member in _members) { - _outMembers.add(_member); + for (var i = 0; i < _members.length; i++) { + List _msg = []; + _msg = json.decode(_messages[i])[_members[i]]; + + _messagesList.add(_msg.map((_temp) { + return Messages.fromMap(_temp); + }).toList()); } return Chats( ownerId: _document.data['ownerId'], - members: _outMembers, + members: _members.map((_member) { + return _member.toString(); + }).toList(), type: _document.data['type'], - messages: _outMessages, + messages: _messagesList, ); } } diff --git a/lib/models/conversations.dart b/lib/models/conversations.dart index 43c684c..b913463 100644 --- a/lib/models/conversations.dart +++ b/lib/models/conversations.dart @@ -3,16 +3,18 @@ import 'package:messaging_app/models/details.dart'; class Conversations { String chatId; Details details; + DateTime? lastModified; Conversations({ required this.chatId, required this.details, + this.lastModified, }); - factory Conversations.fromData(String _chatId, String _details) { - return Conversations( - chatId: _chatId, - details: Details.fromData(_details), - ); - } + factory Conversations.fromData( + String _chatId, String _details, String _lastModified) => + Conversations( + chatId: _chatId, + details: Details.fromData(_details), + lastModified: DateTime.parse(_lastModified)); } diff --git a/lib/models/details.dart b/lib/models/details.dart index 7f91fb1..7bc73ef 100644 --- a/lib/models/details.dart +++ b/lib/models/details.dart @@ -6,6 +6,7 @@ class Details { int unseenCount; String? lastMessage; DateTime? timeStamp; + String? type; Details({ required this.name, @@ -13,6 +14,7 @@ class Details { required this.lastMessage, required this.timeStamp, required this.unseenCount, + required this.type }); factory Details.fromData(String _details) { @@ -22,6 +24,7 @@ class Details { lastMessage: json.decode(_details)['lastMessage'], timeStamp: DateTime.parse(json.decode(_details)['timeStamp'] ?? ""), unseenCount: json.decode(_details)['unseenCount'], + type: json.decode(_details)['type'], ); } } diff --git a/lib/models/messages.dart b/lib/models/messages.dart index 0fd113d..dedb2c0 100644 --- a/lib/models/messages.dart +++ b/lib/models/messages.dart @@ -1,15 +1,10 @@ import 'dart:convert'; -enum MessageType { - Text, - Image, -} - class Messages { String message; String senderId; - MessageType type; - DateTime timeStamp; + String type; + String timeStamp; Messages({ required this.message, @@ -18,16 +13,25 @@ class Messages { required this.timeStamp, }); - factory Messages.fromData(String _message) { - var _messageType = json.decode(_message)['type'] == "text" - ? MessageType.Text - : MessageType.Image; + factory Messages.fromString(String _message) => Messages( + message: json.decode(_message)['message'], + senderId: json.decode(_message)['senderId'], + type: json.decode(_message)['type'], + timeStamp: json.decode(_message)['timeStamp'] ?? "", + ); - return Messages( - message: json.decode(_message)['message'], - senderId: json.decode(_message)['senderId'], - type: _messageType, - timeStamp: DateTime.parse(json.decode(_message)['timeStamp'] ?? ""), - ); - } + factory Messages.fromMap(Map _message) => Messages( + message: _message['message'], + senderId: _message['senderId'], + type: _message['type'], + timeStamp: _message['timeStamp'] ?? "", + ); + + + Map toJson() => { + "message": message, + "senderId": senderId, + "type": type, + "timeStamp": timeStamp, + }; } diff --git a/lib/models/users.dart b/lib/models/users.dart index 5ed8162..3412cee 100644 --- a/lib/models/users.dart +++ b/lib/models/users.dart @@ -1,13 +1,13 @@ import 'package:appwrite/models.dart'; -class Users { +class User { String id; String name; String email; String image; DateTime lastSeen; - Users({ + User({ required this.id, required this.name, required this.email, @@ -15,8 +15,8 @@ class Users { required this.lastSeen, }); - factory Users.fromData(Document _document) { - return Users( + factory User.fromData(Document _document) { + return User( id: _document.data['\$id'], name: _document.data['name'], email: _document.data['email'], diff --git a/lib/pages/conversations_page.dart b/lib/pages/conversations_page.dart index 5905e3b..03f1156 100644 --- a/lib/pages/conversations_page.dart +++ b/lib/pages/conversations_page.dart @@ -1,20 +1,26 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:messaging_app/components/messaging_provider.dart'; import 'package:messaging_app/models/chats.dart'; import 'package:messaging_app/models/messages.dart'; +import 'package:messaging_app/service/media_service.dart'; +import 'package:messaging_app/service/snackbar_service.dart'; import 'package:provider/provider.dart'; import 'package:timeago/timeago.dart' as timeago; class ConversationsPage extends StatefulWidget { ConversationsPage({ super.key, - required this.conversationId, + required this.chatId, // required this.receiverId, required this.chatImage, required this.chatName, }); - String conversationId; + String chatId; // String receiverId; String chatName; String chatImage; @@ -30,7 +36,24 @@ class _ConversationsPageState extends State { late double _deviceHeight; late double _deviceWidth; + + final ScrollController _scrollController; + final GlobalKey _formKey; late MessagingProviders _auth; + List> _messagesArray; + List _messages; + List _members; + int _memberPosition; + String _messageText; + + _ConversationsPageState() + : _messageText = '', + _formKey = GlobalKey(), + _scrollController = ScrollController(), + _messagesArray = [], + _messages = [], + _members = [], + _memberPosition = 0; @override Widget build(BuildContext context) { @@ -52,45 +75,71 @@ class _ConversationsPageState extends State { } Widget _conversationPageUI() { - return Builder(builder: (_context) { - _auth = Provider.of(_context); - return Stack( - clipBehavior: Clip.none, - children: [ - _messageListView(), - _messageInputField(_context), - ], - ); - }); + return Builder( + builder: (_context) { + _auth = Provider.of(_context); + SnackBarService.instance.buildContext = _context; + return Stack( + clipBehavior: Clip.none, + children: [ + _messageListView(), + _messageInputField(_context), + ], + ); + }, + ); } Widget _messageListView() { return Container( - height: _deviceHeight * 0.75, + height: _deviceHeight * 0.80, width: _deviceWidth, child: StreamBuilder( - stream: MessagingProviders.instance.getChat(widget.conversationId), - builder: (_context, _snapshot) { - return _snapshot.hasData - ? ListView.builder( - padding: EdgeInsets.symmetric( - horizontal: 10, - vertical: 10, - ), - itemCount: _snapshot.data!.messages!.length, - itemBuilder: (BuildContext _context, int _index) { - var _message = _snapshot.data!.messages![_index]; - return _messageView(_message); - }, - ) - : Text(""); - }), + stream: MessagingProviders.instance.getMessage(widget.chatId), + builder: (_context, _snapshot) { + if (_snapshot.hasData) { + _members = _snapshot.data!.members; + _memberPosition = _members.indexOf(_auth.user!.$id); + _messages = _snapshot.data!.messages![_memberPosition]; + _messagesArray = _snapshot.data!.messages!; + } + if (_snapshot.hasData) { + if (_messages.isEmpty) { + return Align( + child: Text("No Chat"), + ); + } else { + return ListView.builder( + controller: _scrollController, + padding: EdgeInsets.symmetric( + horizontal: 10, + vertical: 10, + ), + itemCount: _messages.length, + itemBuilder: (BuildContext _context, int _index) { + _messages[_index]; + return _messageView(_messages, _index); + }, + ); + } + } else if (_snapshot.hasError) { + return Align( + child: Text("No Chat"), + ); + } else { + return SpinKitWanderingCubes( + color: Colors.blue, + size: 50.0, + ); + } + }, + ), ); } - Widget _messageView(Messages _message) { + Widget _messageView(List _messages, int _index) { bool _isOwnMessage = false; - if (_message.senderId == _auth.user!.$id) { + if (_messages[_index].senderId == _auth.user!.$id) { _isOwnMessage = true; } @@ -102,8 +151,15 @@ class _ConversationsPageState extends State { _isOwnMessage ? MainAxisAlignment.end : MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ + deleteButton(_isOwnMessage, _messages, _index), _isOwnMessage ? Container() : _imageViewBubble(), - _textMessageBubble(_isOwnMessage, _message), + SizedBox( + width: _deviceWidth * 0.01, + ), + _messages[_index].type == "text" + ? _textMessageBubble(_isOwnMessage, _messages[_index]) + : _imageMessageBubble(_isOwnMessage, _messages[_index]), + deleteButton(!_isOwnMessage, _messages, _index), ], ), ); @@ -114,9 +170,9 @@ class _ConversationsPageState extends State { ? [Colors.blue, Color.fromRGBO(42, 117, 188, 1)] : [Color.fromRGBO(49, 69, 69, 1), Color.fromRGBO(43, 43, 43, 1)]; return Container( - height: _deviceHeight * 0.10, - width: _deviceWidth * 0.75, - padding: EdgeInsets.symmetric(horizontal: 10), + // height: _deviceHeight * 0.10, + width: _deviceWidth * 0.60, + padding: EdgeInsets.symmetric(horizontal: 5, vertical: 5), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), gradient: LinearGradient( @@ -131,12 +187,57 @@ class _ConversationsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_message.message), + SizedBox( + height: 10, + ), Text( - timeago.format(_message.timeStamp), - style: TextStyle( - color: Colors.white70, + timeago.format(DateTime.parse(_message.timeStamp)), + style: TextStyle(color: Colors.white70, fontSize: 10), + ), + ], + ), + ); + } + + Widget _imageMessageBubble(bool _isOwnMessage, Messages _message) { + List _colorScheme = _isOwnMessage + ? [Colors.blue, Color.fromRGBO(42, 117, 188, 1)] + : [Color.fromRGBO(49, 69, 69, 1), Color.fromRGBO(43, 43, 43, 1)]; + return Container( + // height: _deviceHeight * 0.10, + // width: _deviceWidth * 0.75, + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: LinearGradient( + colors: _colorScheme, + stops: [0.25, 0.85], + begin: Alignment.bottomLeft, + end: Alignment.bottomRight), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: _deviceHeight * 0.30, + width: _deviceWidth * 0.40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + image: DecorationImage( + image: NetworkImage(_message.message), + fit: BoxFit.cover, + ), ), ), + SizedBox( + height: 10, + ), + Text( + timeago.format(DateTime.parse(_message.timeStamp)), + style: TextStyle(color: Colors.white70, fontSize: 10), + ), ], ), ); @@ -161,42 +262,49 @@ class _ConversationsPageState extends State { Widget _messageInputField(BuildContext _context) { return Align( alignment: Alignment.bottomCenter, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.max, - children: [ - _messageText(), - _imageUpload(), - _sendMessage(_context), - ], + child: Form( + key: _formKey, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + _messageInput(), + _sendImage(), + _sendText(_context), + ], + ), ), ); } - Widget _messageText() { + Widget _messageInput() { return Container( padding: EdgeInsets.only(left: 10, right: 10), decoration: BoxDecoration( - color: Color.fromRGBO(43, 43, 43, 1), - borderRadius: BorderRadius.circular(30) - ), + color: Color.fromRGBO(43, 43, 43, 1), + borderRadius: BorderRadius.circular(30)), margin: EdgeInsets.symmetric( horizontal: _deviceWidth * 0.04, - vertical: _deviceHeight * 0.02 + vertical: _deviceHeight * 0.02, ), width: _deviceWidth * 0.70, child: TextFormField( + maxLines: null, validator: (_input) { - if (_input == null) { - return ""; + if (_input!.isEmpty) { + SnackBarService.instance + .showSnackBarError("please enter a message"); } + return null; }, onChanged: (_input) { - // + _formKey.currentState?.save(); }, onSaved: (_input) { - // + setState(() { + _messageText = _input!; + }); }, cursorColor: Colors.white, decoration: InputDecoration( @@ -208,12 +316,22 @@ class _ConversationsPageState extends State { ); } - Widget _sendMessage(BuildContext context) { + Widget _sendText(BuildContext _context) { return Container( height: _deviceHeight * 0.045, width: _deviceHeight * 0.045, child: IconButton( - onPressed: () {}, + onPressed: () async { + if (_formKey.currentState!.validate()) { + if (_messageText.isNotEmpty) { + // + await sendMessage('text', _messageText); + + _formKey.currentState?.reset(); + FocusScope.of(_context).unfocus(); + } + } + }, icon: Icon( Icons.send, color: Colors.white, @@ -222,13 +340,84 @@ class _ConversationsPageState extends State { ); } - Widget _imageUpload() { + Widget _sendImage() { return SizedBox( height: _deviceHeight * 0.045, width: _deviceHeight * 0.045, - child: FloatingActionButton(onPressed: () {}, - backgroundColor: Colors.blue, - child: Icon(Icons.camera_enhance),), + child: FloatingActionButton( + onPressed: () async { + XFile? _imageFile = await MediaService.instance.getImageFromLibrary(); + if (_imageFile != null) { + var _url = await MessagingProviders.instance + .uploadUserImage(_imageFile, '_uid'); + + await sendMessage('image', _url); + } + }, + backgroundColor: Colors.blue, + child: Icon( + Icons.camera_enhance, + ), + ), + ); + } + + Widget deleteButton( + bool _isOwnMessage, List _messages, int _index) { + return !_isOwnMessage + ? SizedBox() + : Row( + children: [ + IconButton( + icon: const Icon( + Icons.delete, + size: 15, + ), + onPressed: () async { + List outMessages = []; + _messages.removeAt(_index); + _messagesArray[_memberPosition] = _messages; + for (var i = 0; i < _messagesArray.length; i++) { + outMessages.add( + json.encode( + {_members[i]: _messagesArray[i]}, + ), + ); + } + + await _auth.sendMessage( + widget.chatId, + {"messages": outMessages}, + ); + }, + ), + ], + ); + } + + Future sendMessage(var type, var _message) async { + List outMessages = []; + + for (var i = 0; i < _members.length; i++) { + _messagesArray[i].add( + Messages( + message: _message, + senderId: _auth.user!.$id, + type: type, + timeStamp: DateTime.now().toUtc().toString(), + ), + ); + + outMessages.add( + json.encode( + {_members[i]: _messagesArray[i]}, + ), + ); + } + + await _auth.sendMessage( + widget.chatId, + {"messages": outMessages}, ); } } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 9064b09..d757b1c 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -15,17 +15,38 @@ class HomePage extends StatefulWidget { class _HomePageState extends State with SingleTickerProviderStateMixin { -//TickerProviderStateMixin - // // +//TickerProviderStateMixin + // // late double _deviceHeight; late double _deviceWidth; late final TabController _tabController; + final List _iconTabs = [ + Tab( + icon: Icon( + Icons.people_outline, + size: 30, + ), + ), + Tab( + icon: Icon( + Icons.chat_bubble_outline, + size: 30, + ), + ), + Tab( + icon: Icon( + Icons.person_outline, + size: 30, + ), + ), + ]; + @override void initState() { super.initState(); _tabController = TabController( - length: 3, + length: _iconTabs.length, vsync: this, initialIndex: 1, ); @@ -44,49 +65,45 @@ class _HomePageState extends State _deviceWidth = MediaQuery.of(context).size.width; return Scaffold( - // backgroundColor: Theme.of(context).scaffoldBackgroundColor, - appBar: AppBar( - // backgroundColor: Theme.of(context).scaffoldBackgroundColor, - title: Text("the messenger"), - titleTextStyle: TextStyle(fontSize: 16), - bottom: TabBar( - unselectedLabelColor: Colors.grey, - indicatorColor: Colors.blue, - labelColor: Colors.blue, - controller: _tabController, - tabs: [ - Tab( - icon: Icon( - Icons.people_outline, - size: 30, - ), - ), - Tab( - icon: Icon( - Icons.chat_bubble_outline, - size: 30, - ), - ), - Tab( - icon: Icon( - Icons.person_outline, - size: 30, - ), - ), - ], + // backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + automaticallyImplyLeading: false, + // backgroundColor: Theme.of(context).scaffoldBackgroundColor, + title: Text("the messenger"), + titleTextStyle: TextStyle(fontSize: 16), + bottom: TabBar( + unselectedLabelColor: Colors.grey, + indicatorColor: Colors.blue, + labelColor: Colors.blue, + controller: _tabController, + tabs: _iconTabs, + ), ), - ), - body: _tabBarPages(), - ); + body: _tabBarPages(), + floatingActionButton: _tabController.index == 1 + ? Container() + : FloatingActionButton( + child: Icon(Icons.add), + onPressed: () {}, + )); } Widget _tabBarPages() { return TabBarView( controller: _tabController, children: [ - SearchPage( deviceHeight: _deviceHeight , deviceWidth: _deviceWidth,), - RecentConverstationsPage(_deviceHeight, _deviceWidth), - ProfilePage(_deviceHeight, _deviceWidth), + SearchPage( + deviceHeight: _deviceHeight, + deviceWidth: _deviceWidth, + ), + RecentConverstationsPage( + _deviceHeight, + _deviceWidth, + ), + ProfilePage( + _deviceHeight, + _deviceWidth, + ), ], ); } diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index e752aa9..f0c9962 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -5,7 +5,9 @@ import 'package:messaging_app/service/snackbar_service.dart'; import 'package:provider/provider.dart'; class LoginPage extends StatefulWidget { - const LoginPage({super.key,}); + const LoginPage({ + super.key, + }); @override State createState() { @@ -78,11 +80,11 @@ class _LoginPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Welcome Back!', + 'welcome back!', style: TextStyle(fontSize: 35, fontWeight: FontWeight.w700), ), Text( - 'Please Login to your Account', + 'please login to your account', style: TextStyle(fontSize: 25, fontWeight: FontWeight.w200), ) ], @@ -114,13 +116,15 @@ class _LoginPageState extends State { Widget _emailTextField() { return TextFormField( autocorrect: false, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, style: TextStyle( color: Colors.white, ), validator: (_input) { return _input!.isNotEmpty && _input.contains("@") ? null - : "Please enter a valid Email"; + : "please enter a valid email"; }, onSaved: (_input) { setState(() { @@ -129,7 +133,7 @@ class _LoginPageState extends State { }, cursorColor: Colors.white, decoration: InputDecoration( - hintText: "Email Address", + hintText: "email address", focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.white))), ); @@ -138,11 +142,13 @@ class _LoginPageState extends State { Widget _passwordTextField() { return TextFormField( autocorrect: false, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, style: TextStyle( color: Colors.white, ), validator: (_input) { - return _input!.isNotEmpty ? null : "Please enter a Password"; + return _input!.isNotEmpty ? null : "please enter a aassword"; }, onSaved: (_input) { setState(() { @@ -152,7 +158,7 @@ class _LoginPageState extends State { obscureText: true, cursorColor: Colors.white, decoration: InputDecoration( - hintText: "Password", + hintText: "password", focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.white))), ); @@ -176,7 +182,7 @@ class _LoginPageState extends State { }, color: Colors.blue, child: Text( - 'LOGIN', + 'login', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, @@ -195,7 +201,7 @@ class _LoginPageState extends State { height: _deviceHeight * 0.06, width: _deviceWidth, child: Text( - 'REGISTER', + 'register', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart index ab8b7e5..652a9f8 100644 --- a/lib/pages/profile_page.dart +++ b/lib/pages/profile_page.dart @@ -31,7 +31,7 @@ class ProfilePage extends StatelessWidget { return Builder(builder: (BuildContext _context) { SnackBarService.instance.buildContext = _context; _auth = Provider.of(_context); - return StreamBuilder( + return StreamBuilder( stream: MessagingProviders.instance.getUser(_auth.user!.$id), builder: (_, _snapshot) { var _userData = _snapshot.data; diff --git a/lib/pages/recent_converstations_page.dart b/lib/pages/recent_converstations_page.dart index 1cda9a5..be30f59 100644 --- a/lib/pages/recent_converstations_page.dart +++ b/lib/pages/recent_converstations_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:messaging_app/components/messaging_provider.dart'; @@ -9,37 +11,56 @@ import 'package:provider/provider.dart'; import 'package:timeago/timeago.dart' as timeago; // ignore: must_be_immutable -class RecentConverstationsPage extends StatelessWidget { +class RecentConverstationsPage extends StatefulWidget { // final double _deviceHeight; final double _deviceWidth; + + const RecentConverstationsPage(this._deviceHeight, this._deviceWidth, + {super.key}); + + @override + State createState() => + _RecentConverstationsPageState(); +} + +class _RecentConverstationsPageState extends State { late MessagingProviders _auth; - RecentConverstationsPage(this._deviceHeight, this._deviceWidth); + final myController = StreamController>.broadcast(); + Stream> get messageStream => myController.stream; + + void messager() { + MessagingProviders.instance.getUserConversation(_auth.user!.$id); + } + + void dispose() { + myController.close(); + } @override Widget build(BuildContext context) { - return Container( - height: _deviceHeight, - width: _deviceWidth, + return SizedBox( + height: widget._deviceHeight, + width: widget._deviceWidth, child: ChangeNotifierProvider.value( value: MessagingProviders.instance, - child: _conversationView(), + child: _conversationView(context), ), ); } - Widget _conversationView() { - return Builder(builder: (_context) { + Widget _conversationView(BuildContext _context) { + return Builder(builder: (BuildContext _context) { SnackBarService.instance.buildContext = _context; _auth = Provider.of(_context); return SizedBox( - height: _deviceHeight, - width: _deviceWidth, + height: widget._deviceHeight, + width: widget._deviceWidth, child: StreamBuilder>( stream: MessagingProviders.instance .getUserConversation(_auth.user!.$id), - builder: (_, _snapshot) { + builder: (_context, _snapshot) { if (_snapshot.hasData) { return ListView.builder( itemCount: _snapshot.data!.length, @@ -53,7 +74,7 @@ class RecentConverstationsPage extends StatelessWidget { MaterialPageRoute( builder: (BuildContext _context) { return ConversationsPage( - conversationId: _chatId, + chatId: _chatId, chatName: _details.name, chatImage: _details.image, ); @@ -62,7 +83,11 @@ class RecentConverstationsPage extends StatelessWidget { ); }, title: Text(_details.name), - subtitle: Text(_details.lastMessage!), + subtitle: Text( + _details.type == 'text' + ? _details.lastMessage! + : 'attachment: image', + ), leading: Container( width: 50, height: 50, @@ -78,8 +103,7 @@ class RecentConverstationsPage extends StatelessWidget { ); }, ); - } - if (_snapshot.hasError) { + } else if (_snapshot.hasError) { return Align( child: Text( "no active conversation", diff --git a/lib/pages/registeration_page.dart b/lib/pages/registeration_page.dart index 5239cca..45565f6 100644 --- a/lib/pages/registeration_page.dart +++ b/lib/pages/registeration_page.dart @@ -26,12 +26,10 @@ class _RegisterationPageState extends State { String? _email; String? _password; - late GlobalKey _formKey; + GlobalKey _formKey; late MessagingProviders _auth; - _RegisterationPageState() { - _formKey = GlobalKey(); - } + _RegisterationPageState() : _formKey = GlobalKey(); @override Widget build(BuildContext context) { @@ -56,20 +54,22 @@ class _RegisterationPageState extends State { return Builder(builder: (BuildContext _context) { SnackBarService.instance.buildContext = _context; _auth = Provider.of(_context); - return Container( - // color: Colors.deepOrange, - height: _deviceHeight * 0.75, - padding: EdgeInsets.symmetric(horizontal: _deviceWidth * 0.10), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _headingWidget(), - _inputForm(), - _registerButton(), - _backToLoginPage(), - ], + return SingleChildScrollView( + child: Container( + // color: Colors.deepOrange, + height: _deviceHeight * 0.75, + padding: EdgeInsets.symmetric(horizontal: _deviceWidth * 0.10), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _headingWidget(), + _inputForm(), + _registerButton(), + _backToLoginPage(), + ], + ), ), ); }); @@ -83,11 +83,11 @@ class _RegisterationPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'SignUp to Enjoy!', + 'signup to enjoy!', style: TextStyle(fontSize: 35, fontWeight: FontWeight.w700), ), Text( - 'Please enter your details', + 'please enter your details', style: TextStyle(fontSize: 25, fontWeight: FontWeight.w200), ) ], @@ -148,11 +148,13 @@ class _RegisterationPageState extends State { Widget _nameTextField() { return TextFormField( autocorrect: false, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, style: TextStyle( color: Colors.white, ), validator: (_input) { - return _input!.isNotEmpty ? null : "Please enter a valid Name"; + return _input!.isNotEmpty ? null : "please enter a valid name"; }, onSaved: (_input) { setState(() { @@ -161,7 +163,7 @@ class _RegisterationPageState extends State { }, cursorColor: Colors.white, decoration: InputDecoration( - hintText: "Name", + hintText: "name", focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.white))), ); @@ -170,13 +172,15 @@ class _RegisterationPageState extends State { Widget _emailTextField() { return TextFormField( autocorrect: false, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, style: TextStyle( color: Colors.white, ), validator: (_input) { return _input!.isNotEmpty && _input.contains("@") ? null - : "Please enter a valid Email"; + : "please enter a valid email"; }, onSaved: (_input) { setState(() { @@ -185,7 +189,7 @@ class _RegisterationPageState extends State { }, cursorColor: Colors.white, decoration: InputDecoration( - hintText: "Email", + hintText: "email", focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.white))), ); @@ -194,11 +198,13 @@ class _RegisterationPageState extends State { Widget _passwordTextField() { return TextFormField( autocorrect: false, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.next, style: TextStyle( color: Colors.white, ), validator: (_input) { - return _input!.isNotEmpty ? null : "Please enter a Password"; + return _input!.isNotEmpty ? null : "please enter a password"; }, onSaved: (_input) { setState(() { @@ -208,7 +214,7 @@ class _RegisterationPageState extends State { obscureText: true, cursorColor: Colors.white, decoration: InputDecoration( - hintText: "Password", + hintText: "password", focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.white))), ); @@ -226,7 +232,7 @@ class _RegisterationPageState extends State { onPressed: () { if (_image == null) { SnackBarService.instance - .showSnackBarError("Please Select an Image"); + .showSnackBarError("please select an image"); } if (_formKey.currentState!.validate() && _image != null) { // @@ -247,7 +253,7 @@ class _RegisterationPageState extends State { }, color: Colors.blue, child: Text( - "REGISTER", + "register", style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index d02ae30..6847ea6 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -1,6 +1,10 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:messaging_app/components/messaging_provider.dart'; import 'package:messaging_app/models/users.dart'; +import 'package:messaging_app/pages/conversations_page.dart'; +import 'package:messaging_app/service/navigation_service.dart'; import 'package:provider/provider.dart'; import 'package:timeago/timeago.dart' as timeago; @@ -21,7 +25,8 @@ class SearchPage extends StatefulWidget { class _SearchPageState extends State { late String _searchText; - late List _usersData; + late List _usersData; + // @override void initState() { super.initState(); @@ -31,12 +36,14 @@ class _SearchPageState extends State { @override Widget build(BuildContext context) { - return SizedBox( - height: widget.deviceHeight, - width: widget.deviceWidth, - child: ChangeNotifierProvider.value( - value: MessagingProviders.instance, - child: _searchPaheUI(), + return SingleChildScrollView( + child: SizedBox( + height: widget.deviceHeight, + width: widget.deviceWidth, + child: ChangeNotifierProvider.value( + value: MessagingProviders.instance, + child: _searchPaheUI(), + ), ), ); } @@ -82,7 +89,7 @@ class _SearchPageState extends State { } Widget _searchPageUIList() { - return StreamBuilder>( + return StreamBuilder>( stream: MessagingProviders.instance.getAllUsers(_searchText.trim()), builder: (_, _snapshot) { if (_snapshot.hasData) { @@ -105,6 +112,53 @@ class _SearchPageState extends State { ), ); return ListTile( + enabled: true, + onTap: () async { + String _chatId; + String _linkId = + '${widget._auth.user!.$id}${_userData.id}'; + + _chatId = await MessagingProviders.instance + .getConversationLink( + _linkId.substring(0, 36)); + + if (_chatId.isEmpty) { + List _members = [ + widget._auth.user!.$id, + _userData.id + ]; + + List messagesTemp = []; + messagesTemp = [ + json.encode({ + widget._auth.user!.$id: [], + }), + json.encode({ + _userData.id: [], + }) + ]; + + Map _chat = { + "ownerId": widget._auth.user!.$id, + "members": _members, + "messages": messagesTemp + }; + _chatId = await MessagingProviders.instance + .newMesaage(_chat); + } + + NavigationService.instance.navigateToRoute( + MaterialPageRoute( + builder: (BuildContext _context) { + return ConversationsPage( + chatId: _chatId, + chatName: _userData.name, + chatImage: _userData.image, + ); + }, + ), + ); + }, title: Text(_userData.name), leading: Container( width: 50, diff --git a/pubspec.yaml b/pubspec.yaml index 4e2f707..ab292a1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.8 - appwrite: 13.1.1 + appwrite: 13.0.0 flutter_spinkit: ^5.2.1 provider: ^6.1.2 image_picker: ^1.1.2