From 7e33872886239ef146dedd22bdec9bf761ce9b70 Mon Sep 17 00:00:00 2001 From: Agboola Onasanya Date: Fri, 7 Feb 2025 20:13:07 +0000 Subject: [PATCH] Pages Implementation Implemented Profile and Recent Conversation skeleton pages --- README.md | 6 +- devtools_options.yaml | 1 + lib/components/messaging_provider.dart | 94 +++++++++++++++---- lib/main.dart | 2 +- lib/models/chats.dart | 0 lib/models/conversations.dart | 18 ++++ lib/models/details.dart | 28 ++++++ lib/models/users.dart | 25 +++++ lib/pages/home_page.dart | 71 +++++++++++++- lib/pages/login_page.dart | 7 +- lib/pages/profile_page.dart | 124 +++++++++++++++++++++++++ lib/pages/recent_converstations.dart | 95 +++++++++++++++++++ lib/pages/registeration_page.dart | 9 +- lib/service/navigation_service.dart | 2 +- pubspec.yaml | 29 +----- 15 files changed, 448 insertions(+), 63 deletions(-) create mode 100644 lib/models/chats.dart create mode 100644 lib/models/conversations.dart create mode 100644 lib/models/details.dart create mode 100644 lib/models/users.dart create mode 100644 lib/pages/profile_page.dart create mode 100644 lib/pages/recent_converstations.dart diff --git a/README.md b/README.md index a00d3cc..72918e6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# messaging_app +## Messaging App - messaging_app -A chat app built using Flutter for Both iOS and Android. +# A chat app built using Flutter for Both iOS and Android. -Backend is being covered by Appwrite. It is opensource and can be selfhosted, which is the main reason for using is a backend. +Backend is being covered by Appwrite. It is opensource and can be selfhosted, which is the main reason for it being used a backend. diff --git a/devtools_options.yaml b/devtools_options.yaml index fa0b357..2bc8e05 100644 --- a/devtools_options.yaml +++ b/devtools_options.yaml @@ -1,3 +1,4 @@ description: This file stores settings for Dart & Flutter DevTools. documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states extensions: + - provider: true \ No newline at end of file diff --git a/lib/components/messaging_provider.dart b/lib/components/messaging_provider.dart index 91f2e2b..7ce4c62 100644 --- a/lib/components/messaging_provider.dart +++ b/lib/components/messaging_provider.dart @@ -1,10 +1,11 @@ import 'dart:io'; - import 'package:appwrite/appwrite.dart'; -import 'package:appwrite/models.dart' as models; +import 'package:appwrite/models.dart' as model; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:messaging_app/components/constants.dart'; +import 'package:messaging_app/models/conversations.dart'; +import 'package:messaging_app/models/users.dart'; import 'package:messaging_app/service/navigation_service.dart'; import 'package:messaging_app/service/snackbar_service.dart'; @@ -24,29 +25,31 @@ class MessagingProviders extends ChangeNotifier { late final Storage storage; // Auth Status - AuthStatus? status; - - models.User? user; - models.Session? session; + late AuthStatus status; + late model.User? user; + late model.Session session; static MessagingProviders instance = MessagingProviders(); MessagingProviders() { - init(); - loadUser(); + _init(); + _loadUser(); } - init() { + _init() { client.setEndpoint(endPoint).setProject(projectId).setSelfSigned(); account = Account(client); database = Databases(client); storage = Storage(client); } - loadUser() async { + _loadUser() async { try { user = await account.get(); - status = AuthStatus.authenticated; + if (user != null) { + status = AuthStatus.authenticated; + _autoLogin(); + } } catch (e) { status = AuthStatus.notAuthenticated; } finally { @@ -54,7 +57,11 @@ class MessagingProviders extends ChangeNotifier { } } - void loginWithEmailAndPassword(String _email, String _password) async { + void _autoLogin() { + NavigationService.instance.navigateToReplacement("home"); + } + + void login(String _email, String _password) async { // notifyListeners(); try { @@ -65,17 +72,11 @@ class MessagingProviders extends ChangeNotifier { session = await account.createEmailPasswordSession( email: _email, password: _password); + user = await account.get(); status = AuthStatus.authenticated; - var value = await database.getDocument( - databaseId: databaseId, - collectionId: userCollectionId, - documentId: session!.userId, - ); - SnackBarService.instance - .showSnackBarSuccess("Welcome! ${value.data['name']}"); + SnackBarService.instance.showSnackBarSuccess("Welcome! ${user!.name}"); NavigationService.instance.navigateToReplacement("home"); - account.deleteSession(sessionId: 'current'); } on AppwriteException catch (e) { SnackBarService.instance.showSnackBarError(e.message!); status = AuthStatus.error; @@ -91,6 +92,22 @@ class MessagingProviders extends ChangeNotifier { } } + void logout() async { + try { + await account.deleteSession(sessionId: 'current'); + user = null; + status = AuthStatus.notAuthenticated; +// await onSuccess(); + await NavigationService.instance.navigateToReplacement("login"); + SnackBarService.instance.showSnackBarError("Logged out Successfully"); + } on AppwriteException catch (e) { + SnackBarService.instance.showSnackBarError(e.message!); + } on Exception catch (e) { + SnackBarService.instance.showSnackBarError(e.toString()); + } + notifyListeners(); + } + Future createUser(String _email, String _password, String _name, Future onSuccess(String _uid)) async { notifyListeners(); @@ -149,6 +166,43 @@ class MessagingProviders extends ChangeNotifier { notifyListeners(); } + Stream getUser(String userId) { + var _userData = database.getDocument( + databaseId: databaseId, + collectionId: userCollectionId, + documentId: userId, + ); + + return _userData.asStream().map((_snapshot) { + return Users.fromData(_snapshot); + }); + } + + Stream> getUserConversation(String userId) { +// + List _chatId = []; + List _details = []; + List _returnValue = []; + + var _conversationData = database.getDocument( + databaseId: databaseId, + collectionId: convCollectionId, + documentId: userId.trim(), + ); + + return _conversationData.asStream().map( + (_snapshot) { + _chatId = _snapshot.data['chatId']; + _details = _snapshot.data['details']; + + for (int i = 0; i < _chatId.length; i++) { + _returnValue.add(Conversations.fromData(_chatId[i], _details[i])); + } + return _returnValue; + }, + ); + } + uploadUserImage(XFile? file, String _uid) async { notifyListeners(); try { diff --git a/lib/main.dart b/lib/main.dart index faa56a8..197509d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,7 +22,7 @@ class MessagingApp extends StatelessWidget { theme: ThemeData( brightness: Brightness.dark, scaffoldBackgroundColor: Colors.black, - primaryColor: Color.fromRGBO(42, 117, 188, 1.0), + primaryColor: Color.fromRGBO(42, 117, 188, 1.0), colorScheme: ColorScheme.dark(), useMaterial3: true, ), diff --git a/lib/models/chats.dart b/lib/models/chats.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/models/conversations.dart b/lib/models/conversations.dart new file mode 100644 index 0000000..dc5c1a2 --- /dev/null +++ b/lib/models/conversations.dart @@ -0,0 +1,18 @@ +import 'package:messaging_app/models/details.dart'; + +class Conversations { + String chatId; + Details details; + + Conversations({ + required this.chatId, + required this.details, + }); + + factory Conversations.fromData(String _chatId, String details) { + return Conversations( + chatId: _chatId, + details: Details.fromData(details), + ); + } +} diff --git a/lib/models/details.dart b/lib/models/details.dart new file mode 100644 index 0000000..e226b85 --- /dev/null +++ b/lib/models/details.dart @@ -0,0 +1,28 @@ +import 'dart:convert'; + +class Details { + String name; + String image; + int? unseenCount; + String lastMessage; + DateTime? timeStamp; + + Details({ + required this.name, + required this.image, + required this.lastMessage, + required this.timeStamp, + required this.unseenCount, + }); + + factory Details.fromData(String _data) { + Map _decodedData = json.decode(_data); + return Details( + name: _decodedData['name'], + image: _decodedData['image'], + lastMessage: _decodedData['lastMessage'], + timeStamp: DateTime.parse(_decodedData['timeStamp'] ?? ""), + unseenCount: _decodedData['unseenCount'], + ); + } +} diff --git a/lib/models/users.dart b/lib/models/users.dart new file mode 100644 index 0000000..37022ff --- /dev/null +++ b/lib/models/users.dart @@ -0,0 +1,25 @@ +import 'package:appwrite/models.dart'; + +class Users { + String name; + String email; + String image; + DateTime lastSeen; + + Users({ + required this.name, + required this.email, + required this.image, + required this.lastSeen, + }); + + factory Users.fromData(Document _snapshot) { + return Users( + name: _snapshot.data["name"], + email: _snapshot.data["email"], + image: _snapshot.data["image"], + lastSeen: DateTime.parse(_snapshot.data["lastSeen"]), + ); + } + +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index fcab073..9d7a32c 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:messaging_app/pages/profile_page.dart'; +import 'package:messaging_app/pages/recent_converstations.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -10,17 +12,80 @@ class HomePage extends StatefulWidget { } } -class _HomePageState extends State { +class _HomePageState extends State + with SingleTickerProviderStateMixin { + // // + late double _deviceHeight; + late double _deviceWidth; + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController( + length: 3, + vsync: this, + initialIndex: 1, + ); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { // + _deviceHeight = MediaQuery.of(context).size.height; + _deviceWidth = MediaQuery.of(context).size.width; + return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( backgroundColor: Theme.of(context).scaffoldBackgroundColor, - title: Text("Messaging App"), - titleTextStyle: TextStyle(fontSize: 15), + title: Text("Slick 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: 25, + ), + ), + Tab( + icon: Icon( + Icons.chat_bubble_outline, + size: 25, + ), + ), + Tab( + icon: Icon( + Icons.person_outline, + size: 25, + ), + ), + ], + ), ), + body: _tabBarPages(), + ); + } + + Widget _tabBarPages() { + return TabBarView( + controller: _tabController, + children: [ + Placeholder(), + RecentConverstationsPage(_deviceHeight, _deviceWidth), + ProfilePage(_deviceHeight, _deviceWidth), + ], ); } } diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 799d1eb..3bc0b8d 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -5,7 +5,7 @@ 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() { @@ -41,7 +41,6 @@ class _LoginPageState extends State { value: MessagingProviders.instance, child: _loginPageUI(), ), - //_loginPageUI(), ), ); } @@ -172,12 +171,12 @@ class _LoginPageState extends State { onPressed: () { if (_formKey.currentState!.validate()) { //Login User - _auth.loginWithEmailAndPassword(_email!, _password!); + _auth.login(_email!, _password!); } }, color: Colors.blue, child: Text( - 'Login', + 'Login'.toUpperCase(), style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, diff --git a/lib/pages/profile_page.dart b/lib/pages/profile_page.dart new file mode 100644 index 0000000..c1458de --- /dev/null +++ b/lib/pages/profile_page.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:messaging_app/components/messaging_provider.dart'; +import 'package:messaging_app/models/users.dart'; +import 'package:provider/provider.dart'; + +class ProfilePage extends StatelessWidget { + // + + final double _deviceHeight; + final double _deviceWidth; + late MessagingProviders _auth; + + ProfilePage(this._deviceHeight, this._deviceWidth, {super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: _deviceHeight, + width: _deviceWidth, + child: ChangeNotifierProvider.value( + value: MessagingProviders.instance, + child: _profilePageUI(), + ), + ); + } + + Widget _profilePageUI() { + return Builder(builder: (BuildContext _context) { + _auth = Provider.of(_context); + return StreamBuilder( + stream: MessagingProviders.instance.getUser(_auth.user!.$id), + builder: (_, _snapshot) { + var _userData = _snapshot.data; + return _snapshot.hasData + ? Align( + child: SizedBox( + height: _deviceHeight * 0.50, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _userImageWidget(_userData!.image), + _userNameWidget(_userData.name), + _userEmailWidget(_userData.email), + _logoutButton(), + ], + ), + ), + ) + : SpinKitWanderingCubes( + color: Colors.blue, + size: 50.0, + ); + }); + }); + } + + Widget _userImageWidget(String _image) { + double _imageRadius = _deviceHeight * 0.20; + return Container( + height: _imageRadius, + width: _imageRadius, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(_imageRadius), + image: DecorationImage( + fit: BoxFit.fill, + image: NetworkImage(_image), + ), + ), + ); + } + + Widget _userNameWidget(String _name) { + return Container( + height: _deviceHeight * 0.05, + width: _deviceWidth, + child: Text( + _name, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 30, + ), + ), + ); + } + + Widget _userEmailWidget(String _email) { + return Container( + height: _deviceHeight * 0.03, + width: _deviceWidth, + child: Text( + _email, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white24, + fontSize: 15, + ), + ), + ); + } + + Widget _logoutButton() { + return Container( + height: _deviceHeight * 0.06, + width: _deviceWidth * 0.80, + child: MaterialButton( + onPressed: () { + _auth.logout(); + }, + color: Colors.red, + child: Text( + "Logout".toUpperCase(), + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + ), + ); + } +} diff --git a/lib/pages/recent_converstations.dart b/lib/pages/recent_converstations.dart new file mode 100644 index 0000000..d10366f --- /dev/null +++ b/lib/pages/recent_converstations.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:messaging_app/components/messaging_provider.dart'; +import 'package:messaging_app/models/conversations.dart'; +import 'package:provider/provider.dart'; +import 'package:timeago/timeago.dart' as timeago; + +class RecentConverstationsPage extends StatelessWidget { + // + + final double _deviceHeight; + final double _deviceWidth; + late MessagingProviders _auth; + + RecentConverstationsPage(this._deviceHeight, this._deviceWidth); + + @override + Widget build(BuildContext context) { + return Container( + height: _deviceHeight, + width: _deviceWidth, + child: ChangeNotifierProvider.value( + value: MessagingProviders.instance, + child: _conversationView(), + ), + ); + } + + Widget _conversationView() { + return Builder(builder: (_context) { + _auth = Provider.of(_context); + return Container( + height: _deviceHeight, + width: _deviceWidth, + child: StreamBuilder>( + stream: MessagingProviders.instance + .getUserConversation(_auth.user!.$id), + builder: (_, _snapshot) { + return _snapshot.hasData + ? ListView.builder( + itemCount: _snapshot.data!.length, + itemBuilder: (_context, _index) { + var _details = _snapshot.data![_index].details; + return ListTile( + onTap: () { + // + }, + title: Text(_details.name), + subtitle: Text(_details.lastMessage), + leading: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(100), + image: DecorationImage( + fit: BoxFit.cover, + image: NetworkImage(_details.image), + ), + ), + ), + trailing: _listTrailingWidget(_details.timeStamp), + ); + }, + ) + : SpinKitWanderingCubes( + color: Colors.blue, + size: 50.0, + ); + }), + ); + }); + } + + Widget _listTrailingWidget(DateTime? _date) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + timeago.format(_date!), + style: TextStyle(fontSize: 15), + ), + Container( + height: 12, + width: 12, + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(100), + ), + ) + ], + ); + } +} diff --git a/lib/pages/registeration_page.dart b/lib/pages/registeration_page.dart index 389b60c..6693b3a 100644 --- a/lib/pages/registeration_page.dart +++ b/lib/pages/registeration_page.dart @@ -224,7 +224,6 @@ class _RegisterationPageState extends State { width: _deviceWidth, child: MaterialButton( onPressed: () { - String name = _name!; if (_image == null) { SnackBarService.instance .showSnackBarError("Please Select Image"); @@ -234,12 +233,12 @@ class _RegisterationPageState extends State { _auth.createUser( _email!, _password!, - name, + _name!, (String _uid) async { var _url = await _auth.uploadUserImage(_image, _uid); - _auth.createUserInDB(_uid, _email!, name, _url); + _auth.createUserInDB(_uid, _email!, _name!, _url); if (_auth.status == AuthStatus.authenticated) { - _auth.loginWithEmailAndPassword(_email!, _password!); + _auth.login(_email!, _password!); } // }, @@ -248,7 +247,7 @@ class _RegisterationPageState extends State { }, color: Colors.blue, child: Text( - 'Register', + 'Register'.toUpperCase(), style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, diff --git a/lib/service/navigation_service.dart b/lib/service/navigation_service.dart index 113be00..b0bab56 100644 --- a/lib/service/navigation_service.dart +++ b/lib/service/navigation_service.dart @@ -23,7 +23,7 @@ class NavigationService { } // Manually Call Function - goBack() { + void goBack() { return navigatorKey!.currentState!.pop(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 4185fb1..4e2f707 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,54 +1,31 @@ name: messaging_app description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. +publish_to: 'none' + version: 1.0.0+1 environment: sdk: ^3.6.0 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 appwrite: 13.1.1 - timeago: ^3.7.0 flutter_spinkit: ^5.2.1 provider: ^6.1.2 image_picker: ^1.1.2 + timeago: ^3.7.0 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 flutter: - uses-material-design: true # To add assets to your application, add an assets section, like this: