├── functions ├── .gitignore ├── .eslintrc.js ├── index.js └── package.json ├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── GoogleService-Info.plist │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ └── project.pbxproj ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── firebase_app_id_file.json └── .gitignore ├── .firebaserc ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── manifest.json └── index.html ├── assets └── images │ ├── circle.png │ ├── doodle_bg.png │ ├── splash_dark.png │ └── splash_light.png ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── whatsapp_messenger │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── firebase.json ├── lib ├── feature │ ├── contact │ │ ├── controller │ │ │ └── contacts_controller.dart │ │ ├── widget │ │ │ └── contact_card.dart │ │ ├── repository │ │ │ └── contacts_repository.dart │ │ └── pages │ │ │ └── contact_page.dart │ ├── home │ │ └── pages │ │ │ ├── call_home_page.dart │ │ │ ├── status_home_page.dart │ │ │ ├── home_page.dart │ │ │ └── chat_home_page.dart │ ├── chat │ │ ├── widgets │ │ │ ├── show_date_card.dart │ │ │ ├── custom_list_tile.dart │ │ │ ├── yellow_card.dart │ │ │ ├── message_card.dart │ │ │ └── chat_text_field.dart │ │ ├── controller │ │ │ └── chat_controller.dart │ │ ├── repository │ │ │ └── chat_repository.dart │ │ └── pages │ │ │ ├── chat_page.dart │ │ │ └── profile_page.dart │ ├── welcome │ │ ├── widgets │ │ │ ├── privacy_and_terms.dart │ │ │ └── language_button.dart │ │ └── pages │ │ │ └── welcome_page.dart │ └── auth │ │ ├── widgets │ │ └── custom_text_field.dart │ │ ├── controller │ │ └── auth_controller.dart │ │ ├── pages │ │ ├── image_picker_page.dart │ │ ├── verification_page.dart │ │ ├── login_page.dart │ │ └── user_info_page.dart │ │ └── repository │ │ └── auth_repository.dart ├── common │ ├── utils │ │ └── coloors.dart │ ├── enum │ │ └── message_type.dart │ ├── widgets │ │ ├── custom_elevated_button.dart │ │ ├── short_h_bar.dart │ │ └── custom_icon_button.dart │ ├── helper │ │ ├── last_seen_message.dart │ │ ├── show_alert_dialog.dart │ │ └── show_loading_dialog.dart │ ├── repository │ │ └── firebase_storage_repository.dart │ ├── models │ │ ├── last_message_model.dart │ │ ├── user_model.dart │ │ └── message_model.dart │ ├── theme │ │ ├── light_theme.dart │ │ └── dark_theme.dart │ ├── routes │ │ └── routes.dart │ └── extension │ │ └── custom_theme_extension.dart └── main.dart ├── README.md ├── .gitignore ├── test └── widget_test.dart ├── .metadata ├── analysis_options.yaml └── pubspec.yaml /functions/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "whatsapp-chat-app-id" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/web/icons/Icon-512.png -------------------------------------------------------------------------------- /assets/images/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/assets/images/circle.png -------------------------------------------------------------------------------- /assets/images/doodle_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/assets/images/doodle_bg.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /assets/images/splash_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/assets/images/splash_dark.png -------------------------------------------------------------------------------- /assets/images/splash_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/assets/images/splash_light.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YonasAlem/whatsapp_messenger/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/whatsapp_messenger/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.whatsapp_messenger 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 6 | -------------------------------------------------------------------------------- /functions/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | "eslint:recommended", 9 | "google", 10 | ], 11 | rules: { 12 | quotes: ["error", "double"], 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/firebase_app_id_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_generated_by": "FlutterFire CLI", 3 | "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", 4 | "GOOGLE_APP_ID": "1:157371465900:ios:870c21c7bcdcca35c99f09", 5 | "FIREBASE_PROJECT_ID": "whatsapp-chat-app-id", 6 | "GCM_SENDER_ID": "157371465900" 7 | } -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": [ 3 | { 4 | "source": "functions", 5 | "codebase": "default", 6 | "ignore": [ 7 | "node_modules", 8 | ".git", 9 | "firebase-debug.log", 10 | "firebase-debug.*.log" 11 | ], 12 | "predeploy": [ 13 | "npm --prefix \"%RESOURCE_DIR%\" run lint" 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /lib/feature/contact/controller/contacts_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 2 | import 'package:whatsapp_messenger/feature/contact/repository/contacts_repository.dart'; 3 | 4 | final contactsControllerProvider = FutureProvider( 5 | (ref) { 6 | final contactsRepository = ref.watch(contactsRepositoryProvider); 7 | return contactsRepository.getAllContacts(); 8 | }, 9 | ); 10 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/feature/home/pages/call_home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CallHomePage extends StatelessWidget { 4 | const CallHomePage({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Scaffold( 9 | body: const Center( 10 | child: Text('Call Home Page'), 11 | ), 12 | floatingActionButton: FloatingActionButton( 13 | onPressed: () {}, 14 | child: const Icon(Icons.call), 15 | ), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /lib/feature/home/pages/status_home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class StatusHomePage extends StatelessWidget { 4 | const StatusHomePage({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Scaffold( 9 | body: const Center( 10 | child: Text('Status Home Page'), 11 | ), 12 | floatingActionButton: FloatingActionButton( 13 | onPressed: () {}, 14 | child: const Icon(Icons.edit), 15 | ), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require("firebase-functions"); 2 | const admin = require('firebase-admin'); 3 | admin.initializeApp(); 4 | 5 | const firestore = admin.firestore(); 6 | 7 | exports.onUserStateChange = functions.database.ref('/{uid}/active').onUpdate( 8 | async (change, context) => { 9 | const isActive = change.after.val(); 10 | const firestoreRef = firestore.doc(`users/${context.params.uid}`); 11 | 12 | return firestoreRef.update({ 13 | active: isActive, 14 | lastSeen: Date.now(), 15 | }); 16 | } 17 | ); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # whatsapp_messenger 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /lib/common/utils/coloors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Coloors { 4 | Coloors._(); 5 | 6 | static const Color greenDark = Color(0xFF00A884); 7 | static const Color greenLight = Color(0xFF008069); 8 | static const Color blueDark = Color(0xFF53BDEB); 9 | static const Color blueLight = Color(0xFF027EB5); 10 | static const Color greyDark = Color(0xFF8696A0); 11 | static const Color greyLight = Color(0xFF667781); 12 | static const Color backgroundDark = Color(0xFF111B21); 13 | static const Color backgroundLight = Color(0xFFFFFFFF); 14 | static const Color greyBackground = Color(0xFF202C33); 15 | } 16 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /lib/common/enum/message_type.dart: -------------------------------------------------------------------------------- 1 | enum MessageType { 2 | text('text'), 3 | image('image'), 4 | audio('audio'), 5 | video('video'), 6 | gif('gif'); 7 | 8 | final String type; 9 | 10 | const MessageType(this.type); 11 | } 12 | 13 | extension ConvertMessage on String { 14 | MessageType toEnum() { 15 | switch (this) { 16 | case 'text': 17 | return MessageType.text; 18 | case 'image': 19 | return MessageType.image; 20 | case 'audio': 21 | return MessageType.audio; 22 | case 'video': 23 | return MessageType.video; 24 | case 'gif': 25 | return MessageType.gif; 26 | 27 | default: 28 | return MessageType.text; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /lib/common/widgets/custom_elevated_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomElevatedButton extends StatelessWidget { 4 | const CustomElevatedButton({ 5 | Key? key, 6 | this.buttonWidth, 7 | required this.onPressed, 8 | required this.text, 9 | }) : super(key: key); 10 | 11 | final double? buttonWidth; 12 | final VoidCallback onPressed; 13 | final String text; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return SizedBox( 18 | height: 42, 19 | width: buttonWidth ?? MediaQuery.of(context).size.width - 100, 20 | child: ElevatedButton( 21 | onPressed: onPressed, 22 | child: Text(text), 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "lint": "eslint", 6 | "serve": "firebase emulators:start --only functions", 7 | "shell": "firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "16" 14 | }, 15 | "main": "index.js", 16 | "dependencies": { 17 | "firebase-admin": "^10.0.2", 18 | "firebase-functions": "^3.18.0" 19 | }, 20 | "devDependencies": { 21 | "eslint": "^8.9.0", 22 | "eslint-config-google": "^0.14.0", 23 | "firebase-functions-test": "^0.2.0" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /lib/common/helper/last_seen_message.dart: -------------------------------------------------------------------------------- 1 | String lastSeenMessage(lastSeen) { 2 | DateTime now = DateTime.now(); 3 | Duration differenceDuration = now.difference( 4 | DateTime.fromMillisecondsSinceEpoch(lastSeen), 5 | ); 6 | 7 | String finalMessage = differenceDuration.inSeconds > 59 8 | ? differenceDuration.inMinutes > 59 9 | ? differenceDuration.inHours > 23 10 | ? "${differenceDuration.inDays} ${differenceDuration.inDays == 1 ? 'day' : 'days'}" 11 | : "${differenceDuration.inHours} ${differenceDuration.inHours == 1 ? 'hour' : 'hours'}" 12 | : "${differenceDuration.inMinutes} ${differenceDuration.inMinutes == 1 ? 'minute' : 'minutes'}" 13 | : 'few moments'; 14 | 15 | return finalMessage; 16 | } 17 | -------------------------------------------------------------------------------- /lib/common/widgets/short_h_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 3 | 4 | class ShortHBar extends StatelessWidget { 5 | const ShortHBar({ 6 | super.key, 7 | this.height, 8 | this.width, 9 | this.color, 10 | }); 11 | 12 | final double? height; 13 | final double? width; 14 | final Color? color; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Container( 19 | height: height ?? 4, 20 | width: width ?? 25, 21 | margin: const EdgeInsets.symmetric(vertical: 5), 22 | decoration: BoxDecoration( 23 | color: color ?? context.theme.greyColor!.withOpacity(.2), 24 | borderRadius: BorderRadius.circular(5), 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/feature/chat/widgets/show_date_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intl/intl.dart'; 3 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 4 | 5 | class ShowDateCard extends StatelessWidget { 6 | const ShowDateCard({Key? key, required this.date}) : super(key: key); 7 | 8 | final DateTime date; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Container( 13 | padding: const EdgeInsets.symmetric( 14 | horizontal: 10, 15 | vertical: 5, 16 | ), 17 | margin: const EdgeInsets.symmetric(vertical: 10), 18 | decoration: BoxDecoration( 19 | color: context.theme.receiverChatCardBg, 20 | borderRadius: BorderRadius.circular(10), 21 | ), 22 | child: Text( 23 | DateFormat.yMMMd().format(date), 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/common/repository/firebase_storage_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:firebase_storage/firebase_storage.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | 7 | final firebaseStorageRepositoryProvider = Provider( 8 | (ref) { 9 | return FirebaseStorageRepository(firebaseStorage: FirebaseStorage.instance); 10 | }, 11 | ); 12 | 13 | class FirebaseStorageRepository { 14 | final FirebaseStorage firebaseStorage; 15 | 16 | FirebaseStorageRepository({required this.firebaseStorage}); 17 | 18 | storeFileToFirebase(String ref, var file) async { 19 | UploadTask? uploadTask; 20 | if (file is File) { 21 | uploadTask = firebaseStorage.ref().child(ref).putFile(file); 22 | } 23 | if (file is Uint8List) { 24 | uploadTask = firebaseStorage.ref().child(ref).putData(file); 25 | } 26 | 27 | TaskSnapshot snapshot = await uploadTask!; 28 | String imageUrl = await snapshot.ref.getDownloadURL(); 29 | return imageUrl; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/common/helper/show_alert_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 3 | 4 | showAlertDialog({ 5 | required BuildContext context, 6 | required String message, 7 | String? btnText, 8 | }) { 9 | return showDialog( 10 | context: context, 11 | builder: (context) { 12 | return AlertDialog( 13 | content: Text( 14 | message, 15 | style: TextStyle( 16 | color: context.theme.greyColor, 17 | fontSize: 15, 18 | ), 19 | ), 20 | contentPadding: const EdgeInsets.fromLTRB(20, 20, 20, 0), 21 | actions: [ 22 | TextButton( 23 | onPressed: () => Navigator.pop(context), 24 | child: Text( 25 | btnText ?? "OK", 26 | style: TextStyle( 27 | color: context.theme.circleImageColor, 28 | ), 29 | ), 30 | ), 31 | ], 32 | ); 33 | }, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /lib/feature/chat/widgets/custom_list_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 3 | 4 | class CustomListTile extends StatelessWidget { 5 | const CustomListTile({ 6 | super.key, 7 | required this.title, 8 | required this.leading, 9 | this.subTitle, 10 | this.trailing, 11 | }); 12 | 13 | final String title; 14 | final IconData leading; 15 | final String? subTitle; 16 | final Widget? trailing; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return ListTile( 21 | onTap: () {}, 22 | contentPadding: const EdgeInsets.fromLTRB(25, 5, 10, 5), 23 | title: Text(title), 24 | subtitle: subTitle != null 25 | ? Text( 26 | subTitle!, 27 | style: TextStyle( 28 | color: context.theme.greyColor, 29 | ), 30 | ) 31 | : null, 32 | leading: Icon(leading), 33 | trailing: trailing, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/feature/chat/widgets/yellow_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 3 | 4 | class YellowCard extends StatelessWidget { 5 | const YellowCard({ 6 | Key? key, 7 | }) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Container( 12 | margin: const EdgeInsets.symmetric( 13 | vertical: 10, 14 | horizontal: 30, 15 | ), 16 | padding: const EdgeInsets.all(10), 17 | decoration: BoxDecoration( 18 | color: context.theme.yellowCardBgColor, 19 | borderRadius: BorderRadius.circular(10), 20 | ), 21 | child: Text( 22 | 'Message and calls are end-to-end encrypted. No one outside of this chat, not even WhatsApp, can read or listen to them. Tap to learn more.', 23 | textAlign: TextAlign.center, 24 | style: TextStyle( 25 | fontSize: 13, 26 | color: context.theme.yellowCardTextColor, 27 | ), 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatsapp_messenger", 3 | "short_name": "whatsapp_messenger", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/common/models/last_message_model.dart: -------------------------------------------------------------------------------- 1 | class LastMessageModel { 2 | final String username; 3 | final String profileImageUrl; 4 | final String contactId; 5 | final DateTime timeSent; 6 | final String lastMessage; 7 | 8 | LastMessageModel({ 9 | required this.username, 10 | required this.profileImageUrl, 11 | required this.contactId, 12 | required this.timeSent, 13 | required this.lastMessage, 14 | }); 15 | 16 | Map toMap() { 17 | return { 18 | 'username': username, 19 | 'profileImageUrl': profileImageUrl, 20 | 'contactId': contactId, 21 | 'timeSent': timeSent.millisecondsSinceEpoch, 22 | 'lastMessage': lastMessage, 23 | }; 24 | } 25 | 26 | factory LastMessageModel.fromMap(Map map) { 27 | return LastMessageModel( 28 | username: map['username'] ?? '', 29 | profileImageUrl: map['profileImageUrl'] ?? '', 30 | contactId: map['contactId'] ?? '', 31 | timeSent: DateTime.fromMillisecondsSinceEpoch(map['timeSent']), 32 | lastMessage: map['lastMessage'] ?? '', 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:whatsapp_messenger/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/common/models/user_model.dart: -------------------------------------------------------------------------------- 1 | class UserModel { 2 | final String username; 3 | final String uid; 4 | final String profileImageUrl; 5 | final bool active; 6 | final int lastSeen; 7 | final String phoneNumber; 8 | final List groupId; 9 | 10 | UserModel({ 11 | required this.username, 12 | required this.uid, 13 | required this.profileImageUrl, 14 | required this.active, 15 | required this.lastSeen, 16 | required this.phoneNumber, 17 | required this.groupId, 18 | }); 19 | 20 | Map toMap() { 21 | return { 22 | 'username': username, 23 | 'uid': uid, 24 | 'profileImageUrl': profileImageUrl, 25 | 'active': active, 26 | 'lastSeen': lastSeen, 27 | 'phoneNumber': phoneNumber, 28 | 'groupId': groupId, 29 | }; 30 | } 31 | 32 | factory UserModel.fromMap(Map map) { 33 | return UserModel( 34 | username: map['username'] ?? '', 35 | uid: map['uid'] ?? '', 36 | profileImageUrl: map['profileImageUrl'] ?? '', 37 | active: map['active'] ?? false, 38 | lastSeen: map['lastSeen'] ?? 0, 39 | phoneNumber: map['phoneNumber'] ?? '', 40 | groupId: List.from(map['groupId']), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/common/helper/show_loading_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 3 | import 'package:whatsapp_messenger/common/utils/coloors.dart'; 4 | 5 | showLoadingDialog({ 6 | required BuildContext context, 7 | required String message, 8 | }) async { 9 | return await showDialog( 10 | context: context, 11 | barrierDismissible: false, 12 | builder: (context) { 13 | return AlertDialog( 14 | content: Column( 15 | mainAxisSize: MainAxisSize.min, 16 | children: [ 17 | Row( 18 | children: [ 19 | const CircularProgressIndicator( 20 | color: Coloors.greenDark, 21 | ), 22 | const SizedBox(width: 20), 23 | Expanded( 24 | child: Text( 25 | message, 26 | style: TextStyle( 27 | fontSize: 15, 28 | color: context.theme.greyColor, 29 | height: 1.5, 30 | ), 31 | ), 32 | ), 33 | ], 34 | ), 35 | ], 36 | ), 37 | ); 38 | }, 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /lib/feature/welcome/widgets/privacy_and_terms.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 3 | 4 | class PrivacyAndTerms extends StatelessWidget { 5 | const PrivacyAndTerms({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Padding( 10 | padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), 11 | child: RichText( 12 | textAlign: TextAlign.center, 13 | text: TextSpan( 14 | text: 'Read our ', 15 | style: TextStyle( 16 | color: context.theme.greyColor, 17 | height: 1.5, 18 | ), 19 | children: [ 20 | TextSpan( 21 | text: 'Privacy Policy. ', 22 | style: TextStyle( 23 | color: context.theme.blueColor, 24 | ), 25 | ), 26 | const TextSpan(text: 'Tap "Agree and continue" to accept the '), 27 | TextSpan( 28 | text: 'Terms of Services.', 29 | style: TextStyle( 30 | color: context.theme.blueColor, 31 | ), 32 | ), 33 | ], 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 52b3dc25f6471c27b2144594abb11c741cb88f57 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57 17 | base_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57 18 | - platform: android 19 | create_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57 20 | base_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57 21 | - platform: ios 22 | create_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57 23 | base_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57 24 | - platform: web 25 | create_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57 26 | base_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57 27 | 28 | # User provided section 29 | 30 | # List of Local paths (relative to this file) that should be 31 | # ignored by the migrate tool. 32 | # 33 | # Files that are not part of the templates will be ignored by default. 34 | unmanaged_files: 35 | - 'lib/main.dart' 36 | - 'ios/Runner.xcodeproj/project.pbxproj' 37 | -------------------------------------------------------------------------------- /lib/common/models/message_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:whatsapp_messenger/common/enum/message_type.dart'; 2 | 3 | class MessageModel { 4 | final String senderId; 5 | final String receiverId; 6 | final String textMessage; 7 | final MessageType type; 8 | final DateTime timeSent; 9 | final String messageId; 10 | final bool isSeen; 11 | 12 | MessageModel({ 13 | required this.senderId, 14 | required this.receiverId, 15 | required this.textMessage, 16 | required this.type, 17 | required this.timeSent, 18 | required this.messageId, 19 | required this.isSeen, 20 | }); 21 | 22 | factory MessageModel.fromMap(Map map) { 23 | return MessageModel( 24 | senderId: map["senderId"], 25 | receiverId: map["receiverId"], 26 | textMessage: map["textMessage"], 27 | type: (map["type"] as String).toEnum(), 28 | timeSent: DateTime.fromMillisecondsSinceEpoch(map["timeSent"]), 29 | messageId: map["messageId"], 30 | isSeen: map["isSeen"] ?? false, 31 | ); 32 | } 33 | 34 | Map toMap() { 35 | return { 36 | "senderId": senderId, 37 | "receiverId": receiverId, 38 | "textMessage": textMessage, 39 | "type": type.type, 40 | "timeSent": timeSent.millisecondsSinceEpoch, 41 | "messageId": messageId, 42 | "isSeen": isSeen, 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ios/Runner/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 157371465900-5d92glon5cq0mh0ccg5qc6ijgohlbo8m.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.157371465900-5d92glon5cq0mh0ccg5qc6ijgohlbo8m 9 | API_KEY 10 | AIzaSyCGBNVkNILtkXf9-VuNZZ51IBB18hZngF4 11 | GCM_SENDER_ID 12 | 157371465900 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | com.example.whatsappMessenger 17 | PROJECT_ID 18 | whatsapp-chat-app-id 19 | STORAGE_BUCKET 20 | whatsapp-chat-app-id.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:157371465900:ios:118c8435475ccecec99f09 33 | DATABASE_URL 34 | https://whatsapp-chat-app-id-default-rtdb.firebaseio.com 35 | 36 | -------------------------------------------------------------------------------- /lib/common/widgets/custom_icon_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomIconButton extends StatelessWidget { 4 | const CustomIconButton({ 5 | Key? key, 6 | required this.onPressed, 7 | required this.icon, 8 | this.iconColor, 9 | this.iconSize, 10 | this.minWidth, 11 | this.background, 12 | this.border, 13 | }) : super(key: key); 14 | 15 | final VoidCallback onPressed; 16 | final IconData icon; 17 | final Color? iconColor; 18 | final double? iconSize; 19 | final double? minWidth; 20 | final Color? background; 21 | final BoxBorder? border; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Container( 26 | decoration: BoxDecoration( 27 | color: background, 28 | shape: BoxShape.circle, 29 | border: border, 30 | ), 31 | child: IconButton( 32 | onPressed: onPressed, 33 | splashColor: Colors.transparent, 34 | splashRadius: (minWidth ?? 45) - 25, 35 | iconSize: iconSize ?? 22, 36 | padding: EdgeInsets.zero, 37 | constraints: BoxConstraints( 38 | minWidth: minWidth ?? 45, 39 | minHeight: minWidth ?? 45, 40 | ), 41 | icon: Icon( 42 | icon, 43 | color: iconColor ?? Theme.of(context).appBarTheme.iconTheme?.color, 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Whatsapp Messenger 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | whatsapp_messenger 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /lib/feature/auth/widgets/custom_text_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 3 | import 'package:whatsapp_messenger/common/utils/coloors.dart'; 4 | 5 | class CustomTextField extends StatelessWidget { 6 | const CustomTextField({ 7 | super.key, 8 | this.controller, 9 | this.hintText, 10 | this.readOnly, 11 | this.textAlign, 12 | this.keyboardType, 13 | this.prefixText, 14 | this.onTap, 15 | this.suffixIcon, 16 | this.onChanged, 17 | this.fontSize, 18 | this.autoFocus, 19 | }); 20 | 21 | final TextEditingController? controller; 22 | final String? hintText; 23 | final bool? readOnly; 24 | final TextAlign? textAlign; 25 | final TextInputType? keyboardType; 26 | final String? prefixText; 27 | final VoidCallback? onTap; 28 | final Widget? suffixIcon; 29 | final Function(String)? onChanged; 30 | final double? fontSize; 31 | final bool? autoFocus; 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return TextFormField( 36 | onTap: onTap, 37 | controller: controller, 38 | readOnly: readOnly ?? false, 39 | textAlign: textAlign ?? TextAlign.center, 40 | keyboardType: keyboardType, 41 | onChanged: onChanged, 42 | style: TextStyle(fontSize: fontSize), 43 | autofocus: autoFocus ?? false, 44 | decoration: InputDecoration( 45 | isDense: true, 46 | prefixText: prefixText, 47 | suffix: suffixIcon, 48 | hintText: hintText, 49 | hintStyle: TextStyle(color: context.theme.greyColor), 50 | enabledBorder: const UnderlineInputBorder( 51 | borderSide: BorderSide( 52 | color: Coloors.greenDark, 53 | ), 54 | ), 55 | focusedBorder: const UnderlineInputBorder( 56 | borderSide: BorderSide( 57 | color: Coloors.greenDark, 58 | width: 2, 59 | ), 60 | ), 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 10 | 18 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | whatsapp_messenger 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_core/firebase_core.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_native_splash/flutter_native_splash.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | import 'package:whatsapp_messenger/common/routes/routes.dart'; 6 | import 'package:whatsapp_messenger/common/theme/dark_theme.dart'; 7 | import 'package:whatsapp_messenger/common/theme/light_theme.dart'; 8 | import 'package:whatsapp_messenger/feature/auth/controller/auth_controller.dart'; 9 | import 'package:whatsapp_messenger/feature/home/pages/home_page.dart'; 10 | import 'package:whatsapp_messenger/feature/welcome/pages/welcome_page.dart'; 11 | import 'package:whatsapp_messenger/firebase_options.dart'; 12 | 13 | void main() async { 14 | WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); 15 | FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); 16 | await Firebase.initializeApp( 17 | options: DefaultFirebaseOptions.currentPlatform, 18 | ); 19 | runApp( 20 | const ProviderScope( 21 | child: MyApp(), 22 | ), 23 | ); 24 | } 25 | 26 | class MyApp extends ConsumerWidget { 27 | const MyApp({super.key}); 28 | 29 | @override 30 | Widget build(BuildContext context, WidgetRef ref) { 31 | return MaterialApp( 32 | debugShowCheckedModeBanner: false, 33 | title: 'WhatsApp Me', 34 | theme: lightTheme(), 35 | darkTheme: darkTheme(), 36 | home: ref.watch(userInfoAuthProvider).when( 37 | data: (user) { 38 | FlutterNativeSplash.remove(); 39 | if (user == null) return const WelcomePage(); 40 | return const HomePage(); 41 | }, 42 | error: (error, trace) { 43 | return const Scaffold( 44 | body: Center( 45 | child: Text('Something wrong happened'), 46 | ), 47 | ); 48 | }, 49 | loading: () => const SizedBox(), 50 | ), 51 | onGenerateRoute: Routes.onGenerateRoute, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/feature/welcome/pages/welcome_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 3 | import 'package:whatsapp_messenger/common/routes/routes.dart'; 4 | 5 | import '../../../common/widgets/custom_elevated_button.dart'; 6 | import '../widgets/language_button.dart'; 7 | import '../widgets/privacy_and_terms.dart'; 8 | 9 | class WelcomePage extends StatelessWidget { 10 | const WelcomePage({super.key}); 11 | 12 | navigateToLoginPage(context) { 13 | Navigator.pushNamedAndRemoveUntil( 14 | context, 15 | Routes.login, 16 | (route) => false, 17 | ); 18 | } 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Scaffold( 23 | body: Column( 24 | children: [ 25 | Expanded( 26 | child: Align( 27 | alignment: Alignment.bottomCenter, 28 | child: Padding( 29 | padding: const EdgeInsets.symmetric( 30 | horizontal: 50, 31 | vertical: 10, 32 | ), 33 | child: Image.asset( 34 | 'assets/images/circle.png', 35 | color: context.theme.circleImageColor, 36 | ), 37 | ), 38 | ), 39 | ), 40 | const SizedBox(height: 40), 41 | Expanded( 42 | child: Column( 43 | children: [ 44 | const Text( 45 | 'Welcome to WhatsApp', 46 | style: TextStyle( 47 | fontSize: 22, 48 | fontWeight: FontWeight.bold, 49 | ), 50 | ), 51 | const PrivacyAndTerms(), 52 | CustomElevatedButton( 53 | onPressed: () => navigateToLoginPage(context), 54 | text: 'AGREE AND CONTINUE', 55 | ), 56 | const SizedBox(height: 50), 57 | const LanguageButton(), 58 | ], 59 | )) 60 | ], 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/feature/chat/controller/chat_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:whatsapp_messenger/common/enum/message_type.dart'; 4 | import 'package:whatsapp_messenger/common/models/last_message_model.dart'; 5 | import 'package:whatsapp_messenger/common/models/message_model.dart'; 6 | import 'package:whatsapp_messenger/feature/auth/controller/auth_controller.dart'; 7 | import 'package:whatsapp_messenger/feature/chat/repository/chat_repository.dart'; 8 | 9 | final chatControllerProvider = Provider((ref) { 10 | final chatRepository = ref.watch(chatRepositoryProvider); 11 | return ChatController( 12 | chatRepository: chatRepository, 13 | ref: ref, 14 | ); 15 | }); 16 | 17 | class ChatController { 18 | final ChatRepository chatRepository; 19 | final ProviderRef ref; 20 | 21 | ChatController({required this.chatRepository, required this.ref}); 22 | 23 | void sendFileMessage( 24 | BuildContext context, 25 | var file, 26 | String receiverId, 27 | MessageType messageType, 28 | ) { 29 | ref.read(userInfoAuthProvider).whenData((senderData) { 30 | return chatRepository.sendFileMessage( 31 | file: file, 32 | context: context, 33 | receiverId: receiverId, 34 | senderData: senderData!, 35 | ref: ref, 36 | messageType: messageType, 37 | ); 38 | }); 39 | } 40 | 41 | Stream> getAllOneToOneMessage(String receiverId) { 42 | return chatRepository.getAllOneToOneMessage(receiverId); 43 | } 44 | 45 | Stream> getAllLastMessageList() { 46 | return chatRepository.getAllLastMessageList(); 47 | } 48 | 49 | void sendTextMessage({ 50 | required BuildContext context, 51 | required String textMessage, 52 | required String receiverId, 53 | }) { 54 | ref.read(userInfoAuthProvider).whenData( 55 | (value) => chatRepository.sendTextMessage( 56 | context: context, 57 | textMessage: textMessage, 58 | receiverId: receiverId, 59 | senderData: value!, 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/feature/auth/controller/auth_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:whatsapp_messenger/feature/auth/repository/auth_repository.dart'; 4 | 5 | import '../../../common/models/user_model.dart'; 6 | 7 | final authControllerProvider = Provider((ref) { 8 | final authRepository = ref.watch(authRepositoryProvider); 9 | return AuthController(authRepository: authRepository, ref: ref); 10 | }); 11 | 12 | final userInfoAuthProvider = FutureProvider( 13 | (ref) { 14 | final authController = ref.watch(authControllerProvider); 15 | return authController.getCurrentUserInfo(); 16 | }, 17 | ); 18 | 19 | class AuthController { 20 | final AuthRepository authRepository; 21 | final ProviderRef ref; 22 | 23 | AuthController({required this.authRepository, required this.ref}); 24 | 25 | Stream getUserPresenceStatus({required String uid}) { 26 | return authRepository.getUserPresenceStatus(uid: uid); 27 | } 28 | 29 | void updateUserPresence() { 30 | authRepository.updateUserPresence(); 31 | } 32 | 33 | Future getCurrentUserInfo() async { 34 | UserModel? user = await authRepository.getCurrentUserInfo(); 35 | return user; 36 | } 37 | 38 | void saveUserInfoToFirestore({ 39 | required String username, 40 | required var profileImage, 41 | required BuildContext context, 42 | required bool mounted, 43 | }) { 44 | authRepository.saveUserInfoToFirestore( 45 | username: username, 46 | profileImage: profileImage, 47 | ref: ref, 48 | context: context, 49 | mounted: mounted, 50 | ); 51 | } 52 | 53 | void verifySmsCode({ 54 | required BuildContext context, 55 | required String smsCodeId, 56 | required String smsCode, 57 | required bool mounted, 58 | }) { 59 | authRepository.verifySmsCode( 60 | context: context, 61 | smsCodeId: smsCodeId, 62 | smsCode: smsCode, 63 | mounted: mounted, 64 | ); 65 | } 66 | 67 | void sendSmsCode({ 68 | required BuildContext context, 69 | required String phoneNumber, 70 | }) { 71 | authRepository.sendSmsCode(context: context, phoneNumber: phoneNumber); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/feature/contact/widget/contact_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 4 | import 'package:whatsapp_messenger/common/models/user_model.dart'; 5 | 6 | import '../../../common/utils/coloors.dart'; 7 | 8 | class ContactCard extends StatelessWidget { 9 | const ContactCard({ 10 | Key? key, 11 | required this.contactSource, 12 | required this.onTap, 13 | }) : super(key: key); 14 | 15 | final UserModel contactSource; 16 | final VoidCallback onTap; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return ListTile( 21 | onTap: onTap, 22 | contentPadding: const EdgeInsets.only( 23 | left: 20, 24 | right: 10, 25 | ), 26 | leading: CircleAvatar( 27 | backgroundColor: context.theme.greyColor!.withOpacity(.3), 28 | radius: 20, 29 | backgroundImage: contactSource.profileImageUrl.isNotEmpty 30 | ? CachedNetworkImageProvider(contactSource.profileImageUrl) 31 | : null, 32 | child: contactSource.profileImageUrl.isEmpty 33 | ? const Icon( 34 | Icons.person, 35 | size: 30, 36 | color: Colors.white, 37 | ) 38 | : null, 39 | ), 40 | title: Text( 41 | contactSource.username, 42 | style: const TextStyle( 43 | fontSize: 16, 44 | fontWeight: FontWeight.w500, 45 | ), 46 | ), 47 | subtitle: contactSource.uid.isNotEmpty 48 | ? Text( 49 | "Hey there! I'm using WhatsApp", 50 | style: TextStyle( 51 | color: context.theme.greyColor, 52 | fontWeight: FontWeight.w600, 53 | ), 54 | ) 55 | : null, 56 | trailing: contactSource.uid.isEmpty 57 | ? TextButton( 58 | onPressed: onTap, 59 | style: TextButton.styleFrom( 60 | foregroundColor: Coloors.greenDark, 61 | ), 62 | child: const Text('INVITE'), 63 | ) 64 | : null, 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/feature/contact/repository/contacts_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:flutter_contacts/flutter_contacts.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:whatsapp_messenger/common/models/user_model.dart'; 7 | 8 | final contactsRepositoryProvider = Provider( 9 | (ref) { 10 | return ContactsRepository(firestore: FirebaseFirestore.instance); 11 | }, 12 | ); 13 | 14 | class ContactsRepository { 15 | final FirebaseFirestore firestore; 16 | 17 | ContactsRepository({required this.firestore}); 18 | 19 | Future>> getAllContacts() async { 20 | List firebaseContacts = []; 21 | List phoneContacts = []; 22 | 23 | try { 24 | if (await FlutterContacts.requestPermission()) { 25 | final userCollection = await firestore.collection('users').get(); 26 | final allContactsInThePhone = await FlutterContacts.getContacts( 27 | withProperties: true, 28 | ); 29 | 30 | bool isContactFound = false; 31 | 32 | for (var contact in allContactsInThePhone) { 33 | for (var firebaseContactData in userCollection.docs) { 34 | var firebaseContact = UserModel.fromMap(firebaseContactData.data()); 35 | if (contact.phones[0].number.replaceAll(' ', '') == 36 | firebaseContact.phoneNumber) { 37 | firebaseContacts.add(firebaseContact); 38 | isContactFound = true; 39 | break; 40 | } 41 | } 42 | if (!isContactFound) { 43 | phoneContacts.add( 44 | UserModel( 45 | username: contact.displayName, 46 | uid: '', 47 | profileImageUrl: '', 48 | active: false, 49 | lastSeen: 0, 50 | phoneNumber: contact.phones[0].number.replaceAll(' ', ''), 51 | groupId: [], 52 | ), 53 | ); 54 | } 55 | 56 | isContactFound = false; 57 | } 58 | } 59 | } catch (e) { 60 | log(e.toString()); 61 | } 62 | return [firebaseContacts, phoneContacts]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/feature/home/pages/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | import 'package:whatsapp_messenger/common/widgets/custom_icon_button.dart'; 6 | import 'package:whatsapp_messenger/feature/auth/controller/auth_controller.dart'; 7 | import 'package:whatsapp_messenger/feature/home/pages/call_home_page.dart'; 8 | import 'package:whatsapp_messenger/feature/home/pages/chat_home_page.dart'; 9 | import 'package:whatsapp_messenger/feature/home/pages/status_home_page.dart'; 10 | 11 | class HomePage extends ConsumerStatefulWidget { 12 | const HomePage({super.key}); 13 | 14 | @override 15 | ConsumerState createState() => _HomePageState(); 16 | } 17 | 18 | class _HomePageState extends ConsumerState { 19 | late Timer timer; 20 | 21 | updateUserPresence() { 22 | ref.read(authControllerProvider).updateUserPresence(); 23 | } 24 | 25 | @override 26 | void initState() { 27 | updateUserPresence(); 28 | timer = Timer.periodic( 29 | const Duration(minutes: 1), 30 | (timer) => setState(() {}), 31 | ); 32 | super.initState(); 33 | } 34 | 35 | @override 36 | void dispose() { 37 | timer.cancel(); 38 | super.dispose(); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return DefaultTabController( 44 | length: 3, 45 | child: Scaffold( 46 | appBar: AppBar( 47 | title: const Text( 48 | 'WhatsApp', 49 | style: TextStyle(letterSpacing: 1), 50 | ), 51 | elevation: 1, 52 | actions: [ 53 | CustomIconButton(onPressed: () {}, icon: Icons.search), 54 | CustomIconButton(onPressed: () {}, icon: Icons.more_vert), 55 | ], 56 | bottom: const TabBar( 57 | indicatorWeight: 3, 58 | labelStyle: TextStyle(fontWeight: FontWeight.bold), 59 | splashFactory: NoSplash.splashFactory, 60 | tabs: [ 61 | Tab(text: 'CHATS'), 62 | Tab(text: 'STATUS'), 63 | Tab(text: 'CALLS'), 64 | ], 65 | ), 66 | ), 67 | body: const TabBarView( 68 | children: [ 69 | ChatHomePage(), 70 | StatusHomePage(), 71 | CallHomePage(), 72 | ], 73 | ), 74 | ), 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 33 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.example.whatsapp_messenger" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 50 | minSdkVersion 19 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | multiDexEnabled true 55 | } 56 | 57 | buildTypes { 58 | release { 59 | // TODO: Add your own signing config for the release build. 60 | // Signing with the debug keys for now, so `flutter run --release` works. 61 | signingConfig signingConfigs.debug 62 | } 63 | } 64 | } 65 | 66 | flutter { 67 | source '../..' 68 | } 69 | 70 | dependencies { 71 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 72 | } 73 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/common/theme/light_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:whatsapp_messenger/common/utils/coloors.dart'; 4 | 5 | import '../extension/custom_theme_extension.dart'; 6 | 7 | ThemeData lightTheme() { 8 | final ThemeData base = ThemeData.light(); 9 | return base.copyWith( 10 | backgroundColor: Coloors.backgroundLight, 11 | scaffoldBackgroundColor: Coloors.backgroundLight, 12 | extensions: [CustomThemeExtension.lightMode], 13 | appBarTheme: const AppBarTheme( 14 | backgroundColor: Coloors.greenLight, 15 | titleTextStyle: TextStyle( 16 | fontSize: 18, 17 | fontWeight: FontWeight.w600, 18 | ), 19 | systemOverlayStyle: SystemUiOverlayStyle( 20 | statusBarColor: Colors.transparent, 21 | statusBarIconBrightness: Brightness.dark, 22 | ), 23 | iconTheme: IconThemeData( 24 | color: Colors.white, 25 | ), 26 | ), 27 | tabBarTheme: const TabBarTheme( 28 | indicator: UnderlineTabIndicator( 29 | borderSide: BorderSide( 30 | color: Colors.white, 31 | width: 2, 32 | ), 33 | ), 34 | ), 35 | elevatedButtonTheme: ElevatedButtonThemeData( 36 | style: ElevatedButton.styleFrom( 37 | backgroundColor: Coloors.greenLight, 38 | foregroundColor: Coloors.backgroundLight, 39 | splashFactory: NoSplash.splashFactory, 40 | elevation: 0, 41 | shadowColor: Colors.transparent, 42 | ), 43 | ), 44 | bottomSheetTheme: const BottomSheetThemeData( 45 | backgroundColor: Coloors.backgroundLight, 46 | modalBackgroundColor: Coloors.backgroundLight, 47 | shape: RoundedRectangleBorder( 48 | borderRadius: BorderRadius.vertical( 49 | top: Radius.circular(20), 50 | ), 51 | ), 52 | ), 53 | dialogBackgroundColor: Coloors.backgroundLight, 54 | dialogTheme: DialogTheme( 55 | shape: RoundedRectangleBorder( 56 | borderRadius: BorderRadius.circular(10), 57 | ), 58 | ), 59 | floatingActionButtonTheme: const FloatingActionButtonThemeData( 60 | backgroundColor: Coloors.greenDark, 61 | foregroundColor: Colors.white, 62 | ), 63 | listTileTheme: const ListTileThemeData( 64 | iconColor: Coloors.greyDark, 65 | tileColor: Coloors.backgroundLight, 66 | ), 67 | switchTheme: const SwitchThemeData( 68 | thumbColor: MaterialStatePropertyAll(Color(0xFF83939C)), 69 | trackColor: MaterialStatePropertyAll(Color(0xFFDADFE2)), 70 | ), 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /lib/common/theme/dark_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 4 | import 'package:whatsapp_messenger/common/utils/coloors.dart'; 5 | 6 | ThemeData darkTheme() { 7 | final ThemeData base = ThemeData.dark(); 8 | return base.copyWith( 9 | backgroundColor: Coloors.backgroundDark, 10 | scaffoldBackgroundColor: Coloors.backgroundDark, 11 | extensions: [CustomThemeExtension.darkMode], 12 | appBarTheme: const AppBarTheme( 13 | backgroundColor: Coloors.greyBackground, 14 | titleTextStyle: TextStyle( 15 | fontSize: 18, 16 | fontWeight: FontWeight.w600, 17 | color: Coloors.greyDark, 18 | ), 19 | systemOverlayStyle: SystemUiOverlayStyle( 20 | statusBarColor: Colors.transparent, 21 | statusBarIconBrightness: Brightness.light, 22 | ), 23 | iconTheme: IconThemeData( 24 | color: Coloors.greyDark, 25 | ), 26 | ), 27 | tabBarTheme: const TabBarTheme( 28 | indicator: UnderlineTabIndicator( 29 | borderSide: BorderSide( 30 | color: Coloors.greenDark, 31 | width: 2, 32 | ), 33 | ), 34 | unselectedLabelColor: Coloors.greyDark, 35 | labelColor: Coloors.greenDark, 36 | ), 37 | elevatedButtonTheme: ElevatedButtonThemeData( 38 | style: ElevatedButton.styleFrom( 39 | backgroundColor: Coloors.greenDark, 40 | foregroundColor: Coloors.backgroundDark, 41 | splashFactory: NoSplash.splashFactory, 42 | elevation: 0, 43 | shadowColor: Colors.transparent, 44 | ), 45 | ), 46 | bottomSheetTheme: const BottomSheetThemeData( 47 | backgroundColor: Coloors.greyBackground, 48 | modalBackgroundColor: Coloors.greyBackground, 49 | shape: RoundedRectangleBorder( 50 | borderRadius: BorderRadius.vertical( 51 | top: Radius.circular(20), 52 | ), 53 | ), 54 | ), 55 | dialogBackgroundColor: Coloors.greyBackground, 56 | dialogTheme: DialogTheme( 57 | shape: RoundedRectangleBorder( 58 | borderRadius: BorderRadius.circular(10), 59 | ), 60 | ), 61 | floatingActionButtonTheme: const FloatingActionButtonThemeData( 62 | backgroundColor: Coloors.greenDark, 63 | foregroundColor: Colors.white, 64 | ), 65 | listTileTheme: const ListTileThemeData( 66 | iconColor: Coloors.greyDark, 67 | tileColor: Coloors.backgroundDark, 68 | ), 69 | switchTheme: const SwitchThemeData( 70 | thumbColor: MaterialStatePropertyAll(Coloors.greyDark), 71 | trackColor: MaterialStatePropertyAll(Color(0xFF344047)), 72 | ), 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/common/routes/routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:page_transition/page_transition.dart'; 3 | import 'package:whatsapp_messenger/common/models/user_model.dart'; 4 | import 'package:whatsapp_messenger/feature/auth/pages/login_page.dart'; 5 | import 'package:whatsapp_messenger/feature/auth/pages/user_info_page.dart'; 6 | import 'package:whatsapp_messenger/feature/auth/pages/verification_page.dart'; 7 | import 'package:whatsapp_messenger/feature/chat/pages/chat_page.dart'; 8 | import 'package:whatsapp_messenger/feature/chat/pages/profile_page.dart'; 9 | import 'package:whatsapp_messenger/feature/contact/pages/contact_page.dart'; 10 | import 'package:whatsapp_messenger/feature/home/pages/home_page.dart'; 11 | import 'package:whatsapp_messenger/feature/welcome/pages/welcome_page.dart'; 12 | 13 | class Routes { 14 | static const String welcome = 'welcome'; 15 | static const String login = 'login'; 16 | static const String verification = 'verification'; 17 | static const String userInfo = 'user-info'; 18 | static const String home = 'home'; 19 | static const String contact = 'contact'; 20 | static const String chat = 'chat'; 21 | static const String profile = 'profile'; 22 | 23 | static Route onGenerateRoute(RouteSettings settings) { 24 | switch (settings.name) { 25 | case welcome: 26 | return MaterialPageRoute( 27 | builder: (context) => const WelcomePage(), 28 | ); 29 | case login: 30 | return MaterialPageRoute( 31 | builder: (context) => const LoginPage(), 32 | ); 33 | case verification: 34 | final Map args = settings.arguments as Map; 35 | return MaterialPageRoute( 36 | builder: (context) => VerificationPage( 37 | smsCodeId: args['smsCodeId'], 38 | phoneNumber: args['phoneNumber'], 39 | ), 40 | ); 41 | case userInfo: 42 | final String? profileImageUrl = settings.arguments as String?; 43 | return MaterialPageRoute( 44 | builder: (context) => UserInfoPage( 45 | profileImageUrl: profileImageUrl, 46 | ), 47 | ); 48 | case home: 49 | return MaterialPageRoute( 50 | builder: (context) => const HomePage(), 51 | ); 52 | case contact: 53 | return MaterialPageRoute( 54 | builder: (context) => const ContactPage(), 55 | ); 56 | case chat: 57 | final UserModel user = settings.arguments as UserModel; 58 | return MaterialPageRoute( 59 | builder: (context) => ChatPage(user: user), 60 | ); 61 | case profile: 62 | final UserModel user = settings.arguments as UserModel; 63 | return PageTransition( 64 | child: ProfilePage(user: user), 65 | type: PageTransitionType.fade, 66 | duration: const Duration(milliseconds: 800), 67 | ); 68 | default: 69 | return MaterialPageRoute( 70 | builder: (context) { 71 | return const Scaffold( 72 | body: Center( 73 | child: Text('No Page Route Provided'), 74 | ), 75 | ); 76 | }, 77 | ); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /lib/feature/home/pages/chat_home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:intl/intl.dart'; 5 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 6 | import 'package:whatsapp_messenger/common/models/last_message_model.dart'; 7 | import 'package:whatsapp_messenger/common/models/user_model.dart'; 8 | import 'package:whatsapp_messenger/common/routes/routes.dart'; 9 | import 'package:whatsapp_messenger/common/utils/coloors.dart'; 10 | import 'package:whatsapp_messenger/feature/chat/controller/chat_controller.dart'; 11 | 12 | class ChatHomePage extends ConsumerWidget { 13 | const ChatHomePage({super.key}); 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | return Scaffold( 18 | body: StreamBuilder>( 19 | stream: ref.watch(chatControllerProvider).getAllLastMessageList(), 20 | builder: (_, snapshot) { 21 | if (snapshot.connectionState == ConnectionState.waiting) { 22 | return const Center( 23 | child: CircularProgressIndicator( 24 | color: Coloors.greenDark, 25 | ), 26 | ); 27 | } 28 | return ListView.builder( 29 | itemCount: snapshot.data!.length, 30 | shrinkWrap: true, 31 | itemBuilder: (context, index) { 32 | final lastMessageData = snapshot.data![index]; 33 | return ListTile( 34 | onTap: () { 35 | Navigator.pushNamed( 36 | context, 37 | Routes.chat, 38 | arguments: UserModel( 39 | username: lastMessageData.username, 40 | uid: lastMessageData.contactId, 41 | profileImageUrl: lastMessageData.profileImageUrl, 42 | active: true, 43 | lastSeen: 0, 44 | phoneNumber: '0', 45 | groupId: [], 46 | ), 47 | ); 48 | }, 49 | title: Row( 50 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 51 | children: [ 52 | Text(lastMessageData.username), 53 | Text( 54 | DateFormat.Hm().format(lastMessageData.timeSent), 55 | style: TextStyle( 56 | fontSize: 13, 57 | color: context.theme.greyColor, 58 | ), 59 | ), 60 | ], 61 | ), 62 | subtitle: Padding( 63 | padding: const EdgeInsets.only(top: 3), 64 | child: Text( 65 | lastMessageData.lastMessage, 66 | maxLines: 1, 67 | overflow: TextOverflow.ellipsis, 68 | style: TextStyle(color: context.theme.greyColor), 69 | ), 70 | ), 71 | leading: CircleAvatar( 72 | backgroundImage: CachedNetworkImageProvider( 73 | lastMessageData.profileImageUrl, 74 | ), 75 | radius: 24, 76 | ), 77 | ); 78 | }, 79 | ); 80 | }, 81 | ), 82 | floatingActionButton: FloatingActionButton( 83 | onPressed: () { 84 | Navigator.pushNamed(context, Routes.contact); 85 | }, 86 | child: const Icon(Icons.chat), 87 | ), 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/feature/welcome/widgets/language_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 3 | import 'package:whatsapp_messenger/common/widgets/custom_icon_button.dart'; 4 | 5 | import '../../../common/utils/coloors.dart'; 6 | 7 | class LanguageButton extends StatelessWidget { 8 | const LanguageButton({Key? key}) : super(key: key); 9 | 10 | showBottomSheet(context) { 11 | return showModalBottomSheet( 12 | context: context, 13 | builder: (context) { 14 | return Padding( 15 | padding: const EdgeInsets.symmetric(vertical: 10), 16 | child: Column( 17 | mainAxisSize: MainAxisSize.min, 18 | children: [ 19 | Container( 20 | height: 4, 21 | width: 30, 22 | decoration: BoxDecoration( 23 | color: context.theme.greyColor!.withOpacity(.4), 24 | borderRadius: BorderRadius.circular(5), 25 | ), 26 | ), 27 | const SizedBox(height: 20), 28 | Row( 29 | children: [ 30 | const SizedBox(width: 20), 31 | CustomIconButton( 32 | onPressed: () => Navigator.pop(context), 33 | icon: Icons.close, 34 | ), 35 | const SizedBox(width: 10), 36 | const Text( 37 | 'App Language', 38 | style: TextStyle( 39 | fontSize: 20, 40 | fontWeight: FontWeight.w500, 41 | ), 42 | ), 43 | ], 44 | ), 45 | const SizedBox(height: 10), 46 | Divider( 47 | thickness: .5, 48 | color: context.theme.greyColor!.withOpacity(.3), 49 | ), 50 | RadioListTile( 51 | value: true, 52 | groupValue: true, 53 | onChanged: (value) {}, 54 | activeColor: Coloors.greenDark, 55 | title: const Text('English'), 56 | subtitle: Text( 57 | "(Phone's language)", 58 | style: TextStyle( 59 | color: context.theme.greyColor, 60 | ), 61 | ), 62 | ), 63 | RadioListTile( 64 | value: true, 65 | groupValue: false, 66 | onChanged: (value) {}, 67 | activeColor: Coloors.greenDark, 68 | title: const Text('አማርኛ'), 69 | subtitle: Text( 70 | "Amharic", 71 | style: TextStyle( 72 | color: context.theme.greyColor, 73 | ), 74 | ), 75 | ), 76 | ], 77 | ), 78 | ); 79 | }, 80 | ); 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | return Material( 86 | color: context.theme.langBgColor, 87 | borderRadius: BorderRadius.circular(20), 88 | child: InkWell( 89 | onTap: () => showBottomSheet(context), 90 | borderRadius: BorderRadius.circular(20), 91 | splashFactory: NoSplash.splashFactory, 92 | highlightColor: context.theme.langHightlightColor, 93 | child: Padding( 94 | padding: const EdgeInsets.symmetric( 95 | horizontal: 16, 96 | vertical: 8.0, 97 | ), 98 | child: Row( 99 | mainAxisSize: MainAxisSize.min, 100 | children: const [ 101 | Icon( 102 | Icons.language, 103 | color: Coloors.greenDark, 104 | ), 105 | SizedBox(width: 10), 106 | Text( 107 | 'English', 108 | style: TextStyle( 109 | color: Coloors.greenDark, 110 | ), 111 | ), 112 | SizedBox(width: 10), 113 | Icon( 114 | Icons.keyboard_arrow_down, 115 | color: Coloors.greenDark, 116 | ), 117 | ], 118 | ), 119 | ), 120 | ), 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/feature/auth/pages/image_picker_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:photo_manager/photo_manager.dart'; 5 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 6 | import 'package:whatsapp_messenger/common/widgets/custom_icon_button.dart'; 7 | 8 | class ImagePickerPage extends StatefulWidget { 9 | const ImagePickerPage({super.key}); 10 | 11 | @override 12 | State createState() => _ImagePickerPageState(); 13 | } 14 | 15 | class _ImagePickerPageState extends State { 16 | List imageList = []; 17 | int currentPage = 0; 18 | int? lastPage; 19 | 20 | handleScrollEvent(ScrollNotification scroll) { 21 | if (scroll.metrics.pixels / scroll.metrics.maxScrollExtent <= .33) return; 22 | if (currentPage == lastPage) return; 23 | fetchAllImages(); 24 | } 25 | 26 | fetchAllImages() async { 27 | lastPage = currentPage; 28 | final permission = await PhotoManager.requestPermissionExtend(); 29 | if (!permission.isAuth) return PhotoManager.openSetting(); 30 | 31 | List albums = await PhotoManager.getAssetPathList( 32 | type: RequestType.image, 33 | onlyAll: true, 34 | ); 35 | 36 | List photos = await albums[0].getAssetListPaged( 37 | page: currentPage, 38 | size: 24, 39 | ); 40 | 41 | List temp = []; 42 | 43 | for (var asset in photos) { 44 | temp.add( 45 | FutureBuilder( 46 | future: asset.thumbnailDataWithSize( 47 | const ThumbnailSize(200, 200), 48 | ), 49 | builder: (context, snapshot) { 50 | if (snapshot.connectionState == ConnectionState.done) { 51 | return ClipRRect( 52 | borderRadius: BorderRadius.circular(5), 53 | child: InkWell( 54 | onTap: () => Navigator.pop(context, snapshot.data), 55 | borderRadius: BorderRadius.circular(5), 56 | splashFactory: NoSplash.splashFactory, 57 | child: Container( 58 | margin: const EdgeInsets.all(2), 59 | decoration: BoxDecoration( 60 | border: Border.all( 61 | color: context.theme.greyColor!.withOpacity(.4), 62 | width: 1, 63 | ), 64 | image: DecorationImage( 65 | image: MemoryImage(snapshot.data as Uint8List), 66 | fit: BoxFit.cover, 67 | ), 68 | borderRadius: BorderRadius.circular(5), 69 | ), 70 | ), 71 | ), 72 | ); 73 | } 74 | return const SizedBox(); 75 | }, 76 | ), 77 | ); 78 | } 79 | 80 | setState(() { 81 | imageList.addAll(temp); 82 | currentPage++; 83 | }); 84 | } 85 | 86 | @override 87 | void initState() { 88 | fetchAllImages(); 89 | super.initState(); 90 | } 91 | 92 | @override 93 | Widget build(BuildContext context) { 94 | return Scaffold( 95 | appBar: AppBar( 96 | backgroundColor: Theme.of(context).backgroundColor, 97 | leading: CustomIconButton( 98 | onPressed: () => Navigator.pop(context), 99 | icon: Icons.arrow_back, 100 | ), 101 | title: Text( 102 | 'WhatsApp', 103 | style: TextStyle( 104 | color: context.theme.authAppbarTextColor, 105 | ), 106 | ), 107 | actions: [ 108 | CustomIconButton( 109 | onPressed: () {}, 110 | icon: Icons.more_vert, 111 | ), 112 | ], 113 | ), 114 | body: Padding( 115 | padding: const EdgeInsets.all(5.0), 116 | child: NotificationListener( 117 | onNotification: (ScrollNotification scroll) { 118 | handleScrollEvent(scroll); 119 | return true; 120 | }, 121 | child: GridView.builder( 122 | itemCount: imageList.length, 123 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 124 | crossAxisCount: 3, 125 | ), 126 | itemBuilder: (_, index) { 127 | return imageList[index]; 128 | }, 129 | ), 130 | ), 131 | ), 132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/feature/auth/pages/verification_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 4 | import 'package:whatsapp_messenger/common/widgets/custom_icon_button.dart'; 5 | import 'package:whatsapp_messenger/feature/auth/controller/auth_controller.dart'; 6 | import 'package:whatsapp_messenger/feature/auth/widgets/custom_text_field.dart'; 7 | 8 | class VerificationPage extends ConsumerWidget { 9 | const VerificationPage({ 10 | super.key, 11 | required this.smsCodeId, 12 | required this.phoneNumber, 13 | }); 14 | 15 | final String smsCodeId; 16 | final String phoneNumber; 17 | 18 | void verifySmsCode( 19 | BuildContext context, 20 | WidgetRef ref, 21 | String smsCode, 22 | ) { 23 | ref.read(authControllerProvider).verifySmsCode( 24 | context: context, 25 | smsCodeId: smsCodeId, 26 | smsCode: smsCode, 27 | mounted: true, 28 | ); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context, WidgetRef ref) { 33 | return Scaffold( 34 | appBar: AppBar( 35 | backgroundColor: Theme.of(context).scaffoldBackgroundColor, 36 | elevation: 0, 37 | centerTitle: true, 38 | title: Text( 39 | 'Verify your number', 40 | style: TextStyle( 41 | color: context.theme.authAppbarTextColor, 42 | ), 43 | ), 44 | actions: [ 45 | CustomIconButton( 46 | onPressed: () {}, 47 | icon: Icons.more_vert, 48 | ), 49 | ], 50 | ), 51 | body: SingleChildScrollView( 52 | padding: const EdgeInsets.symmetric(horizontal: 20), 53 | child: Column( 54 | children: [ 55 | Padding( 56 | padding: const EdgeInsets.symmetric(horizontal: 10), 57 | child: RichText( 58 | textAlign: TextAlign.center, 59 | text: TextSpan( 60 | style: TextStyle(color: context.theme.greyColor), 61 | children: [ 62 | const TextSpan( 63 | text: 64 | "You've tried to register +251935838471. before requesting an SMS or Call with your code.", 65 | ), 66 | TextSpan( 67 | text: "Wrong number?", 68 | style: TextStyle( 69 | color: context.theme.blueColor, 70 | ), 71 | ), 72 | ], 73 | ), 74 | ), 75 | ), 76 | const SizedBox(height: 20), 77 | Container( 78 | padding: const EdgeInsets.symmetric(horizontal: 80), 79 | child: CustomTextField( 80 | hintText: "- - - - - -", 81 | fontSize: 30, 82 | autoFocus: true, 83 | keyboardType: TextInputType.number, 84 | onChanged: (value) { 85 | if (value.length == 6) { 86 | return verifySmsCode(context, ref, value); 87 | } 88 | }, 89 | ), 90 | ), 91 | const SizedBox(height: 20), 92 | Text( 93 | 'Enter 6-digit code', 94 | style: TextStyle(color: context.theme.greyColor), 95 | ), 96 | const SizedBox(height: 30), 97 | Row( 98 | children: [ 99 | Icon(Icons.message, color: context.theme.greyColor), 100 | const SizedBox(width: 20), 101 | Text( 102 | 'Resend SMS', 103 | style: TextStyle( 104 | color: context.theme.greyColor, 105 | ), 106 | ), 107 | ], 108 | ), 109 | const SizedBox(height: 10), 110 | Divider( 111 | color: context.theme.greyColor!.withOpacity(.2), 112 | ), 113 | const SizedBox(height: 10), 114 | Row( 115 | children: [ 116 | Icon(Icons.phone, color: context.theme.greyColor), 117 | const SizedBox(width: 20), 118 | Text( 119 | 'Call Me', 120 | style: TextStyle( 121 | color: context.theme.greyColor, 122 | ), 123 | ), 124 | ], 125 | ), 126 | ], 127 | ), 128 | ), 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: whatsapp_messenger 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | # In Windows, build-name is used as the major, minor, and patch parts 19 | # of the product and file versions while build-number is used as the build suffix. 20 | version: 1.0.0+1 21 | 22 | environment: 23 | sdk: '>=2.18.4 <3.0.0' 24 | 25 | # Dependencies specify other packages that your package needs in order to work. 26 | # To automatically upgrade your package dependencies to the latest versions 27 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 28 | # dependencies can be manually updated by changing the version numbers below to 29 | # the latest version available on pub.dev. To see which dependencies have newer 30 | # versions available, run `flutter pub outdated`. 31 | dependencies: 32 | flutter: 33 | sdk: flutter 34 | 35 | 36 | # The following adds the Cupertino Icons font to your application. 37 | # Use with the CupertinoIcons class for iOS style icons. 38 | cupertino_icons: ^1.0.2 39 | flutter_native_splash: ^2.2.14 40 | country_picker: ^2.0.17 41 | firebase_core: ^2.3.0 42 | firebase_auth: ^4.1.3 43 | cloud_firestore: ^4.1.0 44 | firebase_storage: ^11.0.6 45 | flutter_riverpod: ^2.1.1 46 | image_picker: ^0.8.6 47 | photo_manager: ^2.5.0 48 | flutter_contacts: ^1.1.5+1 49 | url_launcher: ^6.1.7 50 | firebase_database: ^10.0.6 51 | page_transition: ^2.0.9 52 | cached_network_image: ^3.2.3 53 | intl: ^0.17.0 54 | uuid: ^3.0.7 55 | custom_clippers: ^2.0.0 56 | shimmer: ^2.0.0 57 | 58 | flutter_native_splash: 59 | color: "#FFFFFF" 60 | color_dark: "#111B21" 61 | image: assets/images/splash_light.png 62 | image_dark: assets/images/splash_dark.png 63 | android_12: 64 | color: "#FFFFFF" 65 | color_dark: "#111B21" 66 | image: assets/images/splash_light.png 67 | image_dark: assets/images/splash_dark.png 68 | icon_background_color: "#FFFFFF" 69 | icon_background_color_dark: "#111B21" 70 | 71 | dev_dependencies: 72 | flutter_test: 73 | sdk: flutter 74 | 75 | # The "flutter_lints" package below contains a set of recommended lints to 76 | # encourage good coding practices. The lint set provided by the package is 77 | # activated in the `analysis_options.yaml` file located at the root of your 78 | # package. See that file for information about deactivating specific lint 79 | # rules and activating additional ones. 80 | flutter_lints: ^2.0.0 81 | 82 | # For information on the generic Dart part of this file, see the 83 | # following page: https://dart.dev/tools/pub/pubspec 84 | 85 | # The following section is specific to Flutter packages. 86 | flutter: 87 | 88 | # The following line ensures that the Material Icons font is 89 | # included with your application, so that you can use the icons in 90 | # the material Icons class. 91 | uses-material-design: true 92 | 93 | # To add assets to your application, add an assets section, like this: 94 | assets: 95 | - assets/images/ 96 | # - images/a_dot_ham.jpeg 97 | 98 | # An image asset can refer to one or more resolution-specific "variants", see 99 | # https://flutter.dev/assets-and-images/#resolution-aware 100 | 101 | # For details regarding adding assets from package dependencies, see 102 | # https://flutter.dev/assets-and-images/#from-packages 103 | 104 | # To add custom fonts to your application, add a fonts section here, 105 | # in this "flutter" section. Each entry in this list should have a 106 | # "family" key with the font family name, and a "fonts" key with a 107 | # list giving the asset and other descriptors for the font. For 108 | # example: 109 | # fonts: 110 | # - family: Schyler 111 | # fonts: 112 | # - asset: fonts/Schyler-Regular.ttf 113 | # - asset: fonts/Schyler-Italic.ttf 114 | # style: italic 115 | # - family: Trajan Pro 116 | # fonts: 117 | # - asset: fonts/TrajanPro.ttf 118 | # - asset: fonts/TrajanPro_Bold.ttf 119 | # weight: 700 120 | # 121 | # For details regarding fonts from package dependencies, 122 | # see https://flutter.dev/custom-fonts/#from-packages 123 | -------------------------------------------------------------------------------- /lib/feature/chat/widgets/message_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:custom_clippers/custom_clippers.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:intl/intl.dart'; 5 | import 'package:whatsapp_messenger/common/enum/message_type.dart' as my_type; 6 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 7 | import 'package:whatsapp_messenger/common/models/message_model.dart'; 8 | 9 | class MessageCard extends StatelessWidget { 10 | const MessageCard({ 11 | Key? key, 12 | required this.isSender, 13 | required this.haveNip, 14 | required this.message, 15 | }) : super(key: key); 16 | 17 | final bool isSender; 18 | final bool haveNip; 19 | final MessageModel message; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Container( 24 | alignment: isSender ? Alignment.centerRight : Alignment.centerLeft, 25 | margin: EdgeInsets.only( 26 | top: 4, 27 | bottom: 4, 28 | left: isSender 29 | ? 80 30 | : haveNip 31 | ? 10 32 | : 15, 33 | right: isSender 34 | ? haveNip 35 | ? 10 36 | : 15 37 | : 80, 38 | ), 39 | child: ClipPath( 40 | clipper: haveNip 41 | ? UpperNipMessageClipperTwo( 42 | isSender ? MessageType.send : MessageType.receive, 43 | nipWidth: 8, 44 | nipHeight: 10, 45 | bubbleRadius: haveNip ? 12 : 0, 46 | ) 47 | : null, 48 | child: Stack( 49 | children: [ 50 | Container( 51 | decoration: BoxDecoration( 52 | color: isSender ? context.theme.senderChatCardBg : context.theme.receiverChatCardBg, 53 | borderRadius: haveNip ? null : BorderRadius.circular(12), 54 | boxShadow: const [ 55 | BoxShadow(color: Colors.black38), 56 | ], 57 | ), 58 | child: Padding( 59 | padding: const EdgeInsets.only(bottom: 5), 60 | child: message.type == my_type.MessageType.image 61 | ? Padding( 62 | padding: const EdgeInsets.only(right: 3, top: 3, left: 3), 63 | child: ClipRRect( 64 | borderRadius: BorderRadius.circular(12), 65 | child: Image( 66 | image: CachedNetworkImageProvider(message.textMessage), 67 | ), 68 | ), 69 | ) 70 | : Padding( 71 | padding: EdgeInsets.only( 72 | top: 8, 73 | bottom: 8, 74 | left: isSender ? 10 : 15, 75 | right: isSender ? 15 : 10, 76 | ), 77 | child: Text( 78 | "${message.textMessage} ", 79 | style: const TextStyle(fontSize: 16), 80 | ), 81 | ), 82 | ), 83 | ), 84 | Positioned( 85 | bottom: message.type == my_type.MessageType.text ? 8 : 4, 86 | right: message.type == my_type.MessageType.text 87 | ? isSender 88 | ? 15 89 | : 10 90 | : 4, 91 | child: message.type == my_type.MessageType.text 92 | ? Text( 93 | DateFormat.Hm().format(message.timeSent), 94 | style: TextStyle( 95 | fontSize: 11, 96 | color: context.theme.greyColor, 97 | ), 98 | ) 99 | : Container( 100 | padding: const EdgeInsets.only(left: 90, right: 10, bottom: 10, top: 14), 101 | decoration: BoxDecoration( 102 | gradient: LinearGradient( 103 | begin: const Alignment(0, -1), 104 | end: const Alignment(1, 1), 105 | colors: [ 106 | context.theme.greyColor!.withOpacity(0), 107 | context.theme.greyColor!.withOpacity(.5), 108 | ], 109 | ), 110 | borderRadius: const BorderRadius.only( 111 | topLeft: Radius.circular(300), 112 | bottomRight: Radius.circular(100), 113 | ), 114 | ), 115 | child: Text( 116 | DateFormat.Hm().format(message.timeSent), 117 | style: const TextStyle( 118 | fontSize: 11, 119 | color: Colors.white, 120 | ), 121 | ), 122 | ), 123 | ) 124 | ], 125 | ), 126 | ), 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/feature/auth/repository/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:firebase_database/firebase_database.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 | import 'package:whatsapp_messenger/common/helper/show_alert_dialog.dart'; 7 | import 'package:whatsapp_messenger/common/helper/show_loading_dialog.dart'; 8 | import 'package:whatsapp_messenger/common/models/user_model.dart'; 9 | import 'package:whatsapp_messenger/common/repository/firebase_storage_repository.dart'; 10 | import 'package:whatsapp_messenger/common/routes/routes.dart'; 11 | 12 | final authRepositoryProvider = Provider((ref) { 13 | return AuthRepository( 14 | auth: FirebaseAuth.instance, 15 | firestore: FirebaseFirestore.instance, 16 | realtime: FirebaseDatabase.instance, 17 | ); 18 | }); 19 | 20 | class AuthRepository { 21 | final FirebaseAuth auth; 22 | final FirebaseFirestore firestore; 23 | final FirebaseDatabase realtime; 24 | 25 | AuthRepository({ 26 | required this.auth, 27 | required this.firestore, 28 | required this.realtime, 29 | }); 30 | 31 | Stream getUserPresenceStatus({required String uid}) { 32 | return firestore 33 | .collection('users') 34 | .doc(uid) 35 | .snapshots() 36 | .map((event) => UserModel.fromMap(event.data()!)); 37 | } 38 | 39 | void updateUserPresence() { 40 | Map online = { 41 | 'active': true, 42 | 'lastSeen': DateTime.now().millisecondsSinceEpoch, 43 | }; 44 | Map offline = { 45 | 'active': false, 46 | 'lastSeen': DateTime.now().millisecondsSinceEpoch, 47 | }; 48 | 49 | final connectedRef = realtime.ref('.info/connected'); 50 | 51 | connectedRef.onValue.listen((event) async { 52 | final isConnected = event.snapshot.value as bool? ?? false; 53 | if (isConnected) { 54 | await realtime.ref().child(auth.currentUser!.uid).update(online); 55 | } else { 56 | realtime 57 | .ref() 58 | .child(auth.currentUser!.uid) 59 | .onDisconnect() 60 | .update(offline); 61 | } 62 | }); 63 | } 64 | 65 | Future getCurrentUserInfo() async { 66 | UserModel? user; 67 | final userInfo = 68 | await firestore.collection('users').doc(auth.currentUser?.uid).get(); 69 | 70 | if (userInfo.data() == null) return user; 71 | user = UserModel.fromMap(userInfo.data()!); 72 | return user; 73 | } 74 | 75 | void saveUserInfoToFirestore({ 76 | required String username, 77 | required var profileImage, 78 | required ProviderRef ref, 79 | required BuildContext context, 80 | required bool mounted, 81 | }) async { 82 | try { 83 | showLoadingDialog( 84 | context: context, 85 | message: "Saving user info ... ", 86 | ); 87 | String uid = auth.currentUser!.uid; 88 | String profileImageUrl = profileImage is String ? profileImage : ''; 89 | if (profileImage != null && profileImage is! String) { 90 | profileImageUrl = await ref 91 | .read(firebaseStorageRepositoryProvider) 92 | .storeFileToFirebase('profileImage/$uid', profileImage); 93 | } 94 | 95 | UserModel user = UserModel( 96 | username: username, 97 | uid: uid, 98 | profileImageUrl: profileImageUrl, 99 | active: true, 100 | lastSeen: DateTime.now().millisecondsSinceEpoch, 101 | phoneNumber: auth.currentUser!.phoneNumber!, 102 | groupId: [], 103 | ); 104 | 105 | await firestore.collection('users').doc(uid).set(user.toMap()); 106 | if (!mounted) return; 107 | 108 | Navigator.pushNamedAndRemoveUntil( 109 | context, 110 | Routes.home, 111 | (route) => false, 112 | ); 113 | } catch (e) { 114 | Navigator.pop(context); 115 | showAlertDialog(context: context, message: e.toString()); 116 | } 117 | } 118 | 119 | void verifySmsCode({ 120 | required BuildContext context, 121 | required String smsCodeId, 122 | required String smsCode, 123 | required bool mounted, 124 | }) async { 125 | try { 126 | showLoadingDialog( 127 | context: context, 128 | message: 'Verifiying code ... ', 129 | ); 130 | final credential = PhoneAuthProvider.credential( 131 | verificationId: smsCodeId, 132 | smsCode: smsCode, 133 | ); 134 | await auth.signInWithCredential(credential); 135 | UserModel? user = await getCurrentUserInfo(); 136 | if (!mounted) return; 137 | Navigator.pushNamedAndRemoveUntil( 138 | context, 139 | Routes.userInfo, 140 | (route) => false, 141 | arguments: user?.profileImageUrl, 142 | ); 143 | } on FirebaseAuthException catch (e) { 144 | Navigator.pop(context); 145 | showAlertDialog(context: context, message: e.toString()); 146 | } 147 | } 148 | 149 | void sendSmsCode({ 150 | required BuildContext context, 151 | required String phoneNumber, 152 | }) async { 153 | try { 154 | showLoadingDialog( 155 | context: context, 156 | message: "Sending a verification code to $phoneNumber", 157 | ); 158 | await auth.verifyPhoneNumber( 159 | phoneNumber: phoneNumber, 160 | verificationCompleted: (PhoneAuthCredential credential) async { 161 | await auth.signInWithCredential(credential); 162 | }, 163 | verificationFailed: (e) { 164 | showAlertDialog(context: context, message: e.toString()); 165 | }, 166 | codeSent: (smsCodeId, resendSmsCodeId) { 167 | Navigator.pushNamedAndRemoveUntil( 168 | context, 169 | Routes.verification, 170 | (route) => false, 171 | arguments: { 172 | 'phoneNumber': phoneNumber, 173 | 'smsCodeId': smsCodeId, 174 | }, 175 | ); 176 | }, 177 | codeAutoRetrievalTimeout: (String smsCodeId) {}, 178 | ); 179 | } on FirebaseAuthException catch (e) { 180 | Navigator.pop(context); 181 | showAlertDialog(context: context, message: e.toString()); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /lib/common/extension/custom_theme_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:whatsapp_messenger/common/utils/coloors.dart'; 3 | 4 | extension ExtendedTheme on BuildContext { 5 | CustomThemeExtension get theme { 6 | return Theme.of(this).extension()!; 7 | } 8 | } 9 | 10 | class CustomThemeExtension extends ThemeExtension { 11 | final Color? circleImageColor; 12 | final Color? greyColor; 13 | final Color? blueColor; 14 | final Color? langBgColor; 15 | final Color? langHightlightColor; 16 | final Color? authAppbarTextColor; 17 | final Color? photoIconBgColor; 18 | final Color? photoIconColor; 19 | final Color? profilePageBg; 20 | final Color? chatTextFieldBg; 21 | final Color? chatPageBgColor; 22 | final Color? chatPageDoodleColor; 23 | final Color? senderChatCardBg; 24 | final Color? receiverChatCardBg; 25 | final Color? yellowCardBgColor; 26 | final Color? yellowCardTextColor; 27 | 28 | const CustomThemeExtension({ 29 | this.circleImageColor, 30 | this.greyColor, 31 | this.blueColor, 32 | this.langBgColor, 33 | this.langHightlightColor, 34 | this.authAppbarTextColor, 35 | this.photoIconBgColor, 36 | this.photoIconColor, 37 | this.profilePageBg, 38 | this.chatTextFieldBg, 39 | this.chatPageBgColor, 40 | this.chatPageDoodleColor, 41 | this.senderChatCardBg, 42 | this.receiverChatCardBg, 43 | this.yellowCardBgColor, 44 | this.yellowCardTextColor, 45 | }); 46 | 47 | static const lightMode = CustomThemeExtension( 48 | circleImageColor: Color(0xFF25D366), 49 | greyColor: Coloors.greyLight, 50 | blueColor: Coloors.blueLight, 51 | langBgColor: Color(0xFFF7F8FA), 52 | langHightlightColor: Color(0xFFE8E8ED), 53 | authAppbarTextColor: Coloors.greenLight, 54 | photoIconBgColor: Color(0xFFF1F1F1), 55 | photoIconColor: Color(0xFF9DAAB3), 56 | profilePageBg: Color(0xFFF7F8FA), 57 | chatTextFieldBg: Colors.white, 58 | chatPageBgColor: Color(0xFFEFE7DE), 59 | chatPageDoodleColor: Colors.white70, 60 | senderChatCardBg: Color(0xFFE7FFDB), 61 | receiverChatCardBg: Color(0xFFFFFFFF), 62 | yellowCardBgColor: Color(0xFFFFEECC), 63 | yellowCardTextColor: Color(0xFF13191C), 64 | ); 65 | 66 | static const darkMode = CustomThemeExtension( 67 | circleImageColor: Coloors.greenDark, 68 | greyColor: Coloors.greyDark, 69 | blueColor: Coloors.blueDark, 70 | langBgColor: Color(0xFF182229), 71 | langHightlightColor: Color(0xFF09141A), 72 | authAppbarTextColor: Color(0xFFE9EDEF), 73 | photoIconBgColor: Color(0xFF283339), 74 | photoIconColor: Color(0xFF61717B), 75 | profilePageBg: Color(0xFF0B141A), 76 | chatTextFieldBg: Coloors.greyBackground, 77 | chatPageBgColor: Color(0xFF081419), 78 | chatPageDoodleColor: Color(0xFF172428), 79 | senderChatCardBg: Color(0xFF005C4B), 80 | receiverChatCardBg: Coloors.greyBackground, 81 | yellowCardBgColor: Color(0xFF222E35), 82 | yellowCardTextColor: Color(0xFFFFD279), 83 | ); 84 | 85 | @override 86 | ThemeExtension copyWith({ 87 | Color? circleImageColor, 88 | Color? greyColor, 89 | Color? blueColor, 90 | Color? langBgColor, 91 | Color? langHightlightColor, 92 | Color? authAppbarTextColor, 93 | Color? photoIconBgColor, 94 | Color? photoIconColor, 95 | Color? profilePageBg, 96 | Color? chatTextFieldBg, 97 | Color? chatPageBgColor, 98 | Color? chatPageDoodleColor, 99 | Color? senderChatCardBg, 100 | Color? receiverChatCardBg, 101 | Color? yellowCardBgColor, 102 | Color? yellowCardTextColor, 103 | }) { 104 | return CustomThemeExtension( 105 | circleImageColor: circleImageColor ?? this.circleImageColor, 106 | greyColor: greyColor ?? this.greyColor, 107 | blueColor: blueColor ?? this.blueColor, 108 | langBgColor: langBgColor ?? this.langBgColor, 109 | langHightlightColor: langHightlightColor ?? this.langHightlightColor, 110 | authAppbarTextColor: authAppbarTextColor ?? this.authAppbarTextColor, 111 | photoIconBgColor: photoIconBgColor ?? this.photoIconBgColor, 112 | photoIconColor: photoIconColor ?? this.photoIconColor, 113 | profilePageBg: profilePageBg ?? this.profilePageBg, 114 | chatTextFieldBg: chatTextFieldBg ?? this.chatTextFieldBg, 115 | chatPageBgColor: chatPageBgColor ?? this.chatPageBgColor, 116 | chatPageDoodleColor: chatPageDoodleColor ?? this.chatPageDoodleColor, 117 | senderChatCardBg: senderChatCardBg ?? this.senderChatCardBg, 118 | receiverChatCardBg: receiverChatCardBg ?? this.receiverChatCardBg, 119 | yellowCardBgColor: yellowCardBgColor ?? this.yellowCardBgColor, 120 | yellowCardTextColor: yellowCardTextColor ?? this.yellowCardTextColor, 121 | ); 122 | } 123 | 124 | @override 125 | ThemeExtension lerp( 126 | ThemeExtension? other, double t) { 127 | if (other is! CustomThemeExtension) return this; 128 | return CustomThemeExtension( 129 | circleImageColor: Color.lerp(circleImageColor, other.circleImageColor, t), 130 | greyColor: Color.lerp(greyColor, other.greyColor, t), 131 | blueColor: Color.lerp(blueColor, other.blueColor, t), 132 | langBgColor: Color.lerp(langBgColor, other.langBgColor, t), 133 | langHightlightColor: 134 | Color.lerp(langHightlightColor, other.langHightlightColor, t), 135 | authAppbarTextColor: 136 | Color.lerp(authAppbarTextColor, other.authAppbarTextColor, t), 137 | photoIconBgColor: Color.lerp(photoIconBgColor, other.photoIconBgColor, t), 138 | photoIconColor: Color.lerp(photoIconColor, other.photoIconColor, t), 139 | profilePageBg: Color.lerp(profilePageBg, other.profilePageBg, t), 140 | chatTextFieldBg: Color.lerp(chatTextFieldBg, other.chatTextFieldBg, t), 141 | chatPageBgColor: Color.lerp(chatPageBgColor, other.chatPageBgColor, t), 142 | senderChatCardBg: Color.lerp(senderChatCardBg, other.senderChatCardBg, t), 143 | yellowCardBgColor: 144 | Color.lerp(yellowCardBgColor, other.yellowCardBgColor, t), 145 | yellowCardTextColor: 146 | Color.lerp(yellowCardTextColor, other.yellowCardTextColor, t), 147 | receiverChatCardBg: 148 | Color.lerp(receiverChatCardBg, other.receiverChatCardBg, t), 149 | chatPageDoodleColor: 150 | Color.lerp(chatPageDoodleColor, other.chatPageDoodleColor, t), 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/feature/contact/pages/contact_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 | import 'package:url_launcher/url_launcher.dart'; 4 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 5 | import 'package:whatsapp_messenger/common/models/user_model.dart'; 6 | import 'package:whatsapp_messenger/common/routes/routes.dart'; 7 | import 'package:whatsapp_messenger/common/utils/coloors.dart'; 8 | import 'package:whatsapp_messenger/common/widgets/custom_icon_button.dart'; 9 | import 'package:whatsapp_messenger/feature/contact/controller/contacts_controller.dart'; 10 | 11 | import '../widget/contact_card.dart'; 12 | 13 | class ContactPage extends ConsumerWidget { 14 | const ContactPage({super.key}); 15 | 16 | shareSmsLink(phoneNumber) async { 17 | Uri sms = Uri.parse( 18 | "sms:$phoneNumber?body=Let's chat on WhatsApp! it's a fast, simple, and secure app we can call each other for free. Get it at https://whatsapp.com/dl/", 19 | ); 20 | if (await launchUrl(sms)) { 21 | } else {} 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context, WidgetRef ref) { 26 | return Scaffold( 27 | appBar: AppBar( 28 | leading: const BackButton(), 29 | title: Column( 30 | crossAxisAlignment: CrossAxisAlignment.start, 31 | children: [ 32 | const Text( 33 | 'Select contact', 34 | style: TextStyle( 35 | color: Colors.white, 36 | ), 37 | ), 38 | const SizedBox(height: 3), 39 | ref.watch(contactsControllerProvider).when( 40 | data: (allContacts) { 41 | return Text( 42 | "${allContacts[0].length} contact${allContacts[0].length == 1 ? '' : 's'}", 43 | style: const TextStyle(fontSize: 12), 44 | ); 45 | }, 46 | error: (e, t) { 47 | return const SizedBox(); 48 | }, 49 | loading: () { 50 | return const Text( 51 | 'counting...', 52 | style: TextStyle(fontSize: 12), 53 | ); 54 | }, 55 | ), 56 | ], 57 | ), 58 | actions: [ 59 | CustomIconButton(onPressed: () {}, icon: Icons.search), 60 | CustomIconButton(onPressed: () {}, icon: Icons.more_vert), 61 | ], 62 | ), 63 | body: ref.watch(contactsControllerProvider).when( 64 | data: (allContacts) { 65 | return ListView.builder( 66 | itemCount: allContacts[0].length + allContacts[1].length, 67 | itemBuilder: (context, index) { 68 | late UserModel firebaseContacts; 69 | late UserModel phoneContacts; 70 | 71 | if (index < allContacts[0].length) { 72 | firebaseContacts = allContacts[0][index]; 73 | } else { 74 | phoneContacts = allContacts[1][index - allContacts[0].length]; 75 | } 76 | return index < allContacts[0].length 77 | ? Column( 78 | crossAxisAlignment: CrossAxisAlignment.start, 79 | children: [ 80 | if (index == 0) 81 | Column( 82 | crossAxisAlignment: CrossAxisAlignment.start, 83 | children: [ 84 | myListTile( 85 | leading: Icons.group, 86 | text: 'New group', 87 | ), 88 | myListTile( 89 | leading: Icons.contacts, 90 | text: 'New contact', 91 | trailing: Icons.qr_code, 92 | ), 93 | Padding( 94 | padding: const EdgeInsets.symmetric( 95 | horizontal: 20, 96 | vertical: 10, 97 | ), 98 | child: Text( 99 | 'Contacts on WhatsApp', 100 | style: TextStyle( 101 | fontWeight: FontWeight.w600, 102 | color: context.theme.greyColor, 103 | ), 104 | ), 105 | ), 106 | ], 107 | ), 108 | ContactCard( 109 | onTap: () { 110 | Navigator.pushNamed( 111 | context, 112 | Routes.chat, 113 | arguments: firebaseContacts, 114 | ); 115 | }, 116 | contactSource: firebaseContacts, 117 | ), 118 | ], 119 | ) 120 | : Column( 121 | crossAxisAlignment: CrossAxisAlignment.start, 122 | children: [ 123 | if (index == allContacts[0].length) 124 | Padding( 125 | padding: const EdgeInsets.symmetric( 126 | horizontal: 20, 127 | vertical: 10, 128 | ), 129 | child: Text( 130 | 'Invite to WhatsApp', 131 | style: TextStyle( 132 | fontWeight: FontWeight.w600, 133 | color: context.theme.greyColor, 134 | ), 135 | ), 136 | ), 137 | ContactCard( 138 | contactSource: phoneContacts, 139 | onTap: () => shareSmsLink(phoneContacts.phoneNumber), 140 | ) 141 | ], 142 | ); 143 | }, 144 | ); 145 | }, 146 | error: (e, t) { 147 | return null; 148 | }, 149 | loading: () { 150 | return Center( 151 | child: CircularProgressIndicator( 152 | color: context.theme.authAppbarTextColor, 153 | ), 154 | ); 155 | }, 156 | ), 157 | ); 158 | } 159 | 160 | ListTile myListTile({ 161 | required IconData leading, 162 | required String text, 163 | IconData? trailing, 164 | }) { 165 | return ListTile( 166 | contentPadding: const EdgeInsets.only(top: 10, left: 20, right: 10), 167 | leading: CircleAvatar( 168 | radius: 20, 169 | backgroundColor: Coloors.greenDark, 170 | child: Icon( 171 | leading, 172 | color: Colors.white, 173 | ), 174 | ), 175 | title: Text( 176 | text, 177 | style: const TextStyle( 178 | fontSize: 16, 179 | fontWeight: FontWeight.w500, 180 | ), 181 | ), 182 | trailing: Icon( 183 | trailing, 184 | color: Coloors.greyDark, 185 | ), 186 | ); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /lib/feature/auth/pages/login_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:country_picker/country_picker.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 5 | import 'package:whatsapp_messenger/common/helper/show_alert_dialog.dart'; 6 | import 'package:whatsapp_messenger/common/widgets/custom_elevated_button.dart'; 7 | import 'package:whatsapp_messenger/feature/auth/controller/auth_controller.dart'; 8 | import 'package:whatsapp_messenger/feature/auth/widgets/custom_text_field.dart'; 9 | 10 | import '../../../common/utils/coloors.dart'; 11 | import '../../../common/widgets/custom_icon_button.dart'; 12 | 13 | class LoginPage extends ConsumerStatefulWidget { 14 | const LoginPage({super.key}); 15 | 16 | @override 17 | ConsumerState createState() => _LoginPageState(); 18 | } 19 | 20 | class _LoginPageState extends ConsumerState { 21 | late TextEditingController countryNameController; 22 | late TextEditingController countryCodeController; 23 | late TextEditingController phoneNumberController; 24 | 25 | sendCodeToPhone() { 26 | final phoneNumber = phoneNumberController.text; 27 | final countryName = countryNameController.text; 28 | final countryCode = countryCodeController.text; 29 | 30 | if (phoneNumber.isEmpty) { 31 | return showAlertDialog( 32 | context: context, 33 | message: "Please enter your phone number", 34 | ); 35 | } else if (phoneNumber.length < 9) { 36 | return showAlertDialog( 37 | context: context, 38 | message: 39 | 'The phone number you entered is too short for the country: $countryName\n\nInclude your area code if you haven\'t', 40 | ); 41 | } else if (phoneNumber.length > 10) { 42 | return showAlertDialog( 43 | context: context, 44 | message: 45 | "The phone number you entered is too long for the country: $countryName", 46 | ); 47 | } 48 | 49 | ref.read(authControllerProvider).sendSmsCode( 50 | context: context, 51 | phoneNumber: "+$countryCode$phoneNumber", 52 | ); 53 | } 54 | 55 | showCountryPickerBottomSheet() { 56 | showCountryPicker( 57 | context: context, 58 | showPhoneCode: true, 59 | favorite: ['ET'], 60 | countryListTheme: CountryListThemeData( 61 | bottomSheetHeight: 600, 62 | backgroundColor: Theme.of(context).backgroundColor, 63 | flagSize: 22, 64 | borderRadius: BorderRadius.circular(20), 65 | textStyle: TextStyle(color: context.theme.greyColor), 66 | inputDecoration: InputDecoration( 67 | labelStyle: TextStyle(color: context.theme.greyColor), 68 | prefixIcon: const Icon( 69 | Icons.language, 70 | color: Coloors.greenDark, 71 | ), 72 | hintText: 'Search country by code or name', 73 | enabledBorder: UnderlineInputBorder( 74 | borderSide: BorderSide( 75 | color: context.theme.greyColor!.withOpacity(.2), 76 | ), 77 | ), 78 | focusedBorder: const UnderlineInputBorder( 79 | borderSide: BorderSide( 80 | color: Coloors.greenDark, 81 | ), 82 | ), 83 | ), 84 | ), 85 | onSelect: (country) { 86 | countryNameController.text = country.name; 87 | countryCodeController.text = country.phoneCode; 88 | }, 89 | ); 90 | } 91 | 92 | @override 93 | void initState() { 94 | countryNameController = TextEditingController(text: 'Ethiopia'); 95 | countryCodeController = TextEditingController(text: '251'); 96 | phoneNumberController = TextEditingController(); 97 | super.initState(); 98 | } 99 | 100 | @override 101 | void dispose() { 102 | countryNameController.dispose(); 103 | countryCodeController.dispose(); 104 | phoneNumberController.dispose(); 105 | super.dispose(); 106 | } 107 | 108 | @override 109 | Widget build(BuildContext context) { 110 | return Scaffold( 111 | appBar: AppBar( 112 | backgroundColor: Theme.of(context).scaffoldBackgroundColor, 113 | elevation: 0, 114 | title: Text( 115 | 'Enter your phone number', 116 | style: TextStyle( 117 | color: context.theme.authAppbarTextColor, 118 | ), 119 | ), 120 | centerTitle: true, 121 | actions: [ 122 | CustomIconButton( 123 | onPressed: () {}, 124 | icon: Icons.more_vert, 125 | ), 126 | ], 127 | ), 128 | body: Column( 129 | children: [ 130 | Padding( 131 | padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), 132 | child: RichText( 133 | textAlign: TextAlign.center, 134 | text: TextSpan( 135 | text: 'WhatsApp will need to verify your number. ', 136 | style: TextStyle( 137 | color: context.theme.greyColor, 138 | height: 1.5, 139 | ), 140 | children: [ 141 | TextSpan( 142 | text: "What's my number?", 143 | style: TextStyle( 144 | color: context.theme.blueColor, 145 | ), 146 | ), 147 | ], 148 | ), 149 | ), 150 | ), 151 | const SizedBox(height: 10), 152 | Padding( 153 | padding: const EdgeInsets.symmetric(horizontal: 50), 154 | child: CustomTextField( 155 | onTap: showCountryPickerBottomSheet, 156 | controller: countryNameController, 157 | readOnly: true, 158 | suffixIcon: const Icon( 159 | Icons.arrow_drop_down, 160 | color: Coloors.greenDark, 161 | size: 22, 162 | ), 163 | ), 164 | ), 165 | const SizedBox(height: 10), 166 | Padding( 167 | padding: const EdgeInsets.symmetric(horizontal: 50), 168 | child: Row( 169 | children: [ 170 | SizedBox( 171 | width: 70, 172 | child: CustomTextField( 173 | onTap: showCountryPickerBottomSheet, 174 | controller: countryCodeController, 175 | prefixText: '+', 176 | readOnly: true, 177 | ), 178 | ), 179 | const SizedBox(width: 10), 180 | Expanded( 181 | child: CustomTextField( 182 | controller: phoneNumberController, 183 | hintText: 'phone number', 184 | textAlign: TextAlign.left, 185 | keyboardType: TextInputType.number, 186 | ), 187 | ), 188 | ], 189 | ), 190 | ), 191 | const SizedBox(height: 20), 192 | Text( 193 | 'Carrier charges may apply', 194 | style: TextStyle( 195 | color: context.theme.greyColor, 196 | ), 197 | ), 198 | ], 199 | ), 200 | floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, 201 | floatingActionButton: CustomElevatedButton( 202 | onPressed: sendCodeToPhone, 203 | text: 'NEXT', 204 | buttonWidth: 90, 205 | ), 206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /lib/feature/chat/repository/chat_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 5 | import 'package:uuid/uuid.dart'; 6 | import 'package:whatsapp_messenger/common/enum/message_type.dart'; 7 | import 'package:whatsapp_messenger/common/helper/show_alert_dialog.dart'; 8 | import 'package:whatsapp_messenger/common/models/last_message_model.dart'; 9 | import 'package:whatsapp_messenger/common/models/message_model.dart'; 10 | import 'package:whatsapp_messenger/common/models/user_model.dart'; 11 | import 'package:whatsapp_messenger/common/repository/firebase_storage_repository.dart'; 12 | 13 | final chatRepositoryProvider = Provider((ref) { 14 | return ChatRepository( 15 | firestore: FirebaseFirestore.instance, 16 | auth: FirebaseAuth.instance, 17 | ); 18 | }); 19 | 20 | class ChatRepository { 21 | final FirebaseFirestore firestore; 22 | final FirebaseAuth auth; 23 | 24 | ChatRepository({required this.firestore, required this.auth}); 25 | 26 | void sendFileMessage({ 27 | required var file, 28 | required BuildContext context, 29 | required String receiverId, 30 | required UserModel senderData, 31 | required Ref ref, 32 | required MessageType messageType, 33 | }) async { 34 | try { 35 | final timeSent = DateTime.now(); 36 | final messageId = const Uuid().v1(); 37 | 38 | final imageUrl = await ref.read(firebaseStorageRepositoryProvider).storeFileToFirebase( 39 | 'chats/${messageType.type}/${senderData.uid}/$receiverId/$messageId', 40 | file, 41 | ); 42 | final userMap = await firestore.collection('users').doc(receiverId).get(); 43 | final receverUserData = UserModel.fromMap(userMap.data()!); 44 | 45 | String lastMessage; 46 | 47 | switch (messageType) { 48 | case MessageType.image: 49 | lastMessage = '📸 Photo message'; 50 | break; 51 | case MessageType.audio: 52 | lastMessage = '📸 Voice message'; 53 | break; 54 | case MessageType.video: 55 | lastMessage = '📸 Video message'; 56 | break; 57 | case MessageType.gif: 58 | lastMessage = '📸 GIF message'; 59 | break; 60 | default: 61 | lastMessage = '📦 GIF message'; 62 | break; 63 | } 64 | 65 | saveToMessageCollection( 66 | receiverId: receiverId, 67 | textMessage: imageUrl, 68 | timeSent: timeSent, 69 | textMessageId: messageId, 70 | senderUsername: senderData.username, 71 | receiverUsername: receverUserData.username, 72 | messageType: messageType, 73 | ); 74 | 75 | saveAsLastMessage( 76 | senderUserData: senderData, 77 | receiverUserData: receverUserData, 78 | lastMessage: lastMessage, 79 | timeSent: timeSent, 80 | receiverId: receiverId, 81 | ); 82 | } catch (e) { 83 | showAlertDialog(context: context, message: e.toString()); 84 | } 85 | } 86 | 87 | Stream> getAllOneToOneMessage(String receiverId) { 88 | return firestore 89 | .collection('users') 90 | .doc(auth.currentUser!.uid) 91 | .collection('chats') 92 | .doc(receiverId) 93 | .collection('messages') 94 | .orderBy('timeSent') 95 | .snapshots() 96 | .map((event) { 97 | List messages = []; 98 | for (var message in event.docs) { 99 | messages.add(MessageModel.fromMap(message.data())); 100 | } 101 | return messages; 102 | }); 103 | } 104 | 105 | Stream> getAllLastMessageList() { 106 | return firestore 107 | .collection('users') 108 | .doc(auth.currentUser!.uid) 109 | .collection('chats') 110 | .snapshots() 111 | .asyncMap((event) async { 112 | List contacts = []; 113 | for (var document in event.docs) { 114 | final lastMessage = LastMessageModel.fromMap(document.data()); 115 | final userData = await firestore.collection('users').doc(lastMessage.contactId).get(); 116 | final user = UserModel.fromMap(userData.data()!); 117 | contacts.add( 118 | LastMessageModel( 119 | username: user.username, 120 | profileImageUrl: user.profileImageUrl, 121 | contactId: lastMessage.contactId, 122 | timeSent: lastMessage.timeSent, 123 | lastMessage: lastMessage.lastMessage, 124 | ), 125 | ); 126 | } 127 | return contacts; 128 | }); 129 | } 130 | 131 | void sendTextMessage({ 132 | required BuildContext context, 133 | required String textMessage, 134 | required String receiverId, 135 | required UserModel senderData, 136 | }) async { 137 | try { 138 | final timeSent = DateTime.now(); 139 | final receiverDataMap = await firestore.collection('users').doc(receiverId).get(); 140 | final receiverData = UserModel.fromMap(receiverDataMap.data()!); 141 | final textMessageId = const Uuid().v1(); 142 | 143 | saveToMessageCollection( 144 | receiverId: receiverId, 145 | textMessage: textMessage, 146 | timeSent: timeSent, 147 | textMessageId: textMessageId, 148 | senderUsername: senderData.username, 149 | receiverUsername: receiverData.username, 150 | messageType: MessageType.text, 151 | ); 152 | 153 | saveAsLastMessage( 154 | senderUserData: senderData, 155 | receiverUserData: receiverData, 156 | lastMessage: textMessage, 157 | timeSent: timeSent, 158 | receiverId: receiverId, 159 | ); 160 | } catch (e) { 161 | showAlertDialog(context: context, message: e.toString()); 162 | } 163 | } 164 | 165 | void saveToMessageCollection({ 166 | required String receiverId, 167 | required String textMessage, 168 | required DateTime timeSent, 169 | required String textMessageId, 170 | required String senderUsername, 171 | required String receiverUsername, 172 | required MessageType messageType, 173 | }) async { 174 | final message = MessageModel( 175 | senderId: auth.currentUser!.uid, 176 | receiverId: receiverId, 177 | textMessage: textMessage, 178 | type: messageType, 179 | timeSent: timeSent, 180 | messageId: textMessageId, 181 | isSeen: false, 182 | ); 183 | 184 | // sender 185 | await firestore 186 | .collection('users') 187 | .doc(auth.currentUser!.uid) 188 | .collection('chats') 189 | .doc(receiverId) 190 | .collection('messages') 191 | .doc(textMessageId) 192 | .set(message.toMap()); 193 | 194 | // receiver 195 | await firestore 196 | .collection('users') 197 | .doc(receiverId) 198 | .collection('chats') 199 | .doc(auth.currentUser!.uid) 200 | .collection('messages') 201 | .doc(textMessageId) 202 | .set(message.toMap()); 203 | } 204 | 205 | void saveAsLastMessage({ 206 | required UserModel senderUserData, 207 | required UserModel receiverUserData, 208 | required String lastMessage, 209 | required DateTime timeSent, 210 | required String receiverId, 211 | }) async { 212 | final receiverLastMessage = LastMessageModel( 213 | username: senderUserData.username, 214 | profileImageUrl: senderUserData.profileImageUrl, 215 | contactId: senderUserData.uid, 216 | timeSent: timeSent, 217 | lastMessage: lastMessage, 218 | ); 219 | 220 | await firestore 221 | .collection('users') 222 | .doc(receiverId) 223 | .collection('chats') 224 | .doc(auth.currentUser!.uid) 225 | .set(receiverLastMessage.toMap()); 226 | 227 | final senderLastMessage = LastMessageModel( 228 | username: receiverUserData.username, 229 | profileImageUrl: receiverUserData.profileImageUrl, 230 | contactId: receiverUserData.uid, 231 | timeSent: timeSent, 232 | lastMessage: lastMessage, 233 | ); 234 | 235 | await firestore 236 | .collection('users') 237 | .doc(auth.currentUser!.uid) 238 | .collection('chats') 239 | .doc(receiverId) 240 | .set(senderLastMessage.toMap()); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /lib/feature/auth/pages/user_info_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | import 'dart:io'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 7 | import 'package:image_picker/image_picker.dart'; 8 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 9 | import 'package:whatsapp_messenger/common/helper/show_alert_dialog.dart'; 10 | import 'package:whatsapp_messenger/common/utils/coloors.dart'; 11 | import 'package:whatsapp_messenger/common/widgets/custom_elevated_button.dart'; 12 | import 'package:whatsapp_messenger/common/widgets/custom_icon_button.dart'; 13 | import 'package:whatsapp_messenger/common/widgets/short_h_bar.dart'; 14 | import 'package:whatsapp_messenger/feature/auth/controller/auth_controller.dart'; 15 | import 'package:whatsapp_messenger/feature/auth/pages/image_picker_page.dart'; 16 | import 'package:whatsapp_messenger/feature/auth/widgets/custom_text_field.dart'; 17 | 18 | class UserInfoPage extends ConsumerStatefulWidget { 19 | const UserInfoPage({super.key, this.profileImageUrl}); 20 | 21 | final String? profileImageUrl; 22 | 23 | @override 24 | ConsumerState createState() => _UserInfoPageState(); 25 | } 26 | 27 | class _UserInfoPageState extends ConsumerState { 28 | File? imageCamera; 29 | Uint8List? imageGallery; 30 | 31 | late TextEditingController usernameController; 32 | 33 | saveUserDataToFirebase() { 34 | String username = usernameController.text; 35 | if (username.isEmpty) { 36 | return showAlertDialog( 37 | context: context, 38 | message: 'Please provide a username', 39 | ); 40 | } else if (username.length < 3 || username.length > 20) { 41 | return showAlertDialog( 42 | context: context, 43 | message: 'A username length should be between 3-20', 44 | ); 45 | } 46 | 47 | ref.read(authControllerProvider).saveUserInfoToFirestore( 48 | username: username, 49 | profileImage: 50 | imageCamera ?? imageGallery ?? widget.profileImageUrl ?? '', 51 | context: context, 52 | mounted: mounted, 53 | ); 54 | } 55 | 56 | imagePickerTypeBottomSheet() { 57 | return showModalBottomSheet( 58 | context: context, 59 | builder: (context) { 60 | return Column( 61 | mainAxisSize: MainAxisSize.min, 62 | children: [ 63 | const ShortHBar(), 64 | Row( 65 | children: [ 66 | const SizedBox(width: 20), 67 | const Text( 68 | 'Profile photo', 69 | style: TextStyle( 70 | fontSize: 20, 71 | fontWeight: FontWeight.w500, 72 | ), 73 | ), 74 | const Spacer(), 75 | CustomIconButton( 76 | onPressed: () => Navigator.pop(context), 77 | icon: Icons.close, 78 | ), 79 | const SizedBox(width: 15), 80 | ], 81 | ), 82 | Divider( 83 | color: context.theme.greyColor!.withOpacity(.3), 84 | ), 85 | const SizedBox(height: 5), 86 | Row( 87 | children: [ 88 | const SizedBox(width: 20), 89 | imagePickerIcon( 90 | onTap: pickmageFromCamera, 91 | icon: Icons.camera_alt_rounded, 92 | text: 'Camera', 93 | ), 94 | const SizedBox(width: 15), 95 | imagePickerIcon( 96 | onTap: () async { 97 | log('hello'); 98 | Navigator.pop(context); 99 | final image = await Navigator.push( 100 | context, 101 | MaterialPageRoute( 102 | builder: (context) => const ImagePickerPage(), 103 | ), 104 | ); 105 | if (image == null) return; 106 | setState(() { 107 | imageGallery = image; 108 | imageCamera = null; 109 | }); 110 | }, 111 | icon: Icons.photo_camera_back_rounded, 112 | text: 'Gallery', 113 | ), 114 | ], 115 | ), 116 | const SizedBox(height: 15), 117 | ], 118 | ); 119 | }, 120 | ); 121 | } 122 | 123 | pickmageFromCamera() async { 124 | try { 125 | Navigator.pop(context); 126 | final image = await ImagePicker().pickImage(source: ImageSource.camera); 127 | setState(() { 128 | imageCamera = File(image!.path); 129 | imageGallery = null; 130 | }); 131 | } catch (e) { 132 | showAlertDialog(context: context, message: e.toString()); 133 | } 134 | } 135 | 136 | imagePickerIcon({ 137 | required VoidCallback onTap, 138 | required IconData icon, 139 | required String text, 140 | }) { 141 | return Column( 142 | children: [ 143 | CustomIconButton( 144 | onPressed: onTap, 145 | icon: icon, 146 | iconColor: Coloors.greenDark, 147 | minWidth: 50, 148 | border: Border.all( 149 | color: context.theme.greyColor!.withOpacity(.2), 150 | width: 1, 151 | ), 152 | ), 153 | const SizedBox(height: 5), 154 | Text( 155 | text, 156 | style: TextStyle( 157 | color: context.theme.greyColor, 158 | ), 159 | ), 160 | ], 161 | ); 162 | } 163 | 164 | @override 165 | void initState() { 166 | usernameController = TextEditingController(); 167 | super.initState(); 168 | } 169 | 170 | @override 171 | void dispose() { 172 | usernameController.dispose(); 173 | super.dispose(); 174 | } 175 | 176 | @override 177 | Widget build(BuildContext context) { 178 | return Scaffold( 179 | appBar: AppBar( 180 | backgroundColor: Theme.of(context).backgroundColor, 181 | elevation: 0, 182 | title: Text( 183 | 'Profile info', 184 | style: TextStyle( 185 | color: context.theme.authAppbarTextColor, 186 | ), 187 | ), 188 | centerTitle: true, 189 | ), 190 | body: SingleChildScrollView( 191 | padding: const EdgeInsets.symmetric(horizontal: 20), 192 | child: Column( 193 | children: [ 194 | Text( 195 | 'Please provide your name and an optional profile photo', 196 | textAlign: TextAlign.center, 197 | style: TextStyle( 198 | color: context.theme.greyColor, 199 | ), 200 | ), 201 | const SizedBox(height: 40), 202 | GestureDetector( 203 | onTap: imagePickerTypeBottomSheet, 204 | child: Container( 205 | padding: const EdgeInsets.all(26), 206 | decoration: BoxDecoration( 207 | shape: BoxShape.circle, 208 | color: context.theme.photoIconBgColor, 209 | border: Border.all( 210 | color: imageCamera == null && imageGallery == null 211 | ? Colors.transparent 212 | : context.theme.greyColor!.withOpacity(.4), 213 | ), 214 | image: imageCamera != null || 215 | imageGallery != null || 216 | widget.profileImageUrl != null 217 | ? DecorationImage( 218 | fit: BoxFit.cover, 219 | image: imageGallery != null 220 | ? MemoryImage(imageGallery!) 221 | : widget.profileImageUrl != null 222 | ? NetworkImage(widget.profileImageUrl!) 223 | : FileImage(imageCamera!) as ImageProvider, 224 | ) 225 | : null, 226 | ), 227 | child: Padding( 228 | padding: const EdgeInsets.only(bottom: 3, right: 3), 229 | child: Icon( 230 | Icons.add_a_photo_rounded, 231 | size: 48, 232 | color: imageCamera == null && 233 | imageGallery == null && 234 | widget.profileImageUrl == null 235 | ? context.theme.photoIconColor 236 | : Colors.transparent, 237 | ), 238 | ), 239 | ), 240 | ), 241 | const SizedBox(height: 40), 242 | Row( 243 | children: [ 244 | const SizedBox(width: 20), 245 | Expanded( 246 | child: CustomTextField( 247 | controller: usernameController, 248 | hintText: 'Type your name here', 249 | textAlign: TextAlign.start, 250 | autoFocus: true, 251 | ), 252 | ), 253 | const SizedBox(width: 10), 254 | Icon( 255 | Icons.emoji_emotions_outlined, 256 | color: context.theme.photoIconColor, 257 | ), 258 | const SizedBox(width: 10), 259 | ], 260 | ), 261 | ], 262 | ), 263 | ), 264 | floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, 265 | floatingActionButton: CustomElevatedButton( 266 | onPressed: saveUserDataToFirebase, 267 | text: 'NEXT', 268 | buttonWidth: 90, 269 | ), 270 | ); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /lib/feature/chat/pages/chat_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:cached_network_image/cached_network_image.dart'; 4 | import 'package:custom_clippers/custom_clippers.dart'; 5 | import 'package:firebase_auth/firebase_auth.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 8 | import 'package:shimmer/shimmer.dart'; 9 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 10 | import 'package:whatsapp_messenger/common/models/user_model.dart'; 11 | import 'package:whatsapp_messenger/common/routes/routes.dart'; 12 | import 'package:whatsapp_messenger/common/widgets/custom_icon_button.dart'; 13 | import 'package:whatsapp_messenger/feature/auth/controller/auth_controller.dart'; 14 | import 'package:whatsapp_messenger/feature/chat/controller/chat_controller.dart'; 15 | import 'package:whatsapp_messenger/feature/chat/widgets/chat_text_field.dart'; 16 | import 'package:whatsapp_messenger/feature/chat/widgets/message_card.dart'; 17 | import 'package:whatsapp_messenger/feature/chat/widgets/show_date_card.dart'; 18 | import 'package:whatsapp_messenger/feature/chat/widgets/yellow_card.dart'; 19 | 20 | import '../../../common/helper/last_seen_message.dart'; 21 | 22 | final pageStorageBucket = PageStorageBucket(); 23 | 24 | class ChatPage extends ConsumerWidget { 25 | ChatPage({super.key, required this.user}); 26 | 27 | final UserModel user; 28 | final ScrollController scrollController = ScrollController(); 29 | 30 | @override 31 | Widget build(BuildContext context, WidgetRef ref) { 32 | return Scaffold( 33 | backgroundColor: context.theme.chatPageBgColor, 34 | appBar: AppBar( 35 | leading: InkWell( 36 | onTap: () { 37 | Navigator.pop(context); 38 | }, 39 | borderRadius: BorderRadius.circular(20), 40 | child: Row( 41 | children: [ 42 | const Icon(Icons.arrow_back), 43 | Hero( 44 | tag: 'profile', 45 | child: Container( 46 | width: 32, 47 | decoration: BoxDecoration( 48 | shape: BoxShape.circle, 49 | image: DecorationImage( 50 | image: CachedNetworkImageProvider(user.profileImageUrl), 51 | ), 52 | ), 53 | ), 54 | ), 55 | ], 56 | ), 57 | ), 58 | title: InkWell( 59 | onTap: () { 60 | Navigator.pushNamed( 61 | context, 62 | Routes.profile, 63 | arguments: user, 64 | ); 65 | }, 66 | child: Padding( 67 | padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 5), 68 | child: Column( 69 | crossAxisAlignment: CrossAxisAlignment.start, 70 | children: [ 71 | Text( 72 | user.username, 73 | style: const TextStyle( 74 | fontSize: 18, 75 | color: Colors.white, 76 | ), 77 | ), 78 | const SizedBox(height: 3), 79 | StreamBuilder( 80 | stream: ref.read(authControllerProvider).getUserPresenceStatus(uid: user.uid), 81 | builder: (_, snapshot) { 82 | if (snapshot.connectionState != ConnectionState.active) { 83 | return const SizedBox(); 84 | } 85 | 86 | final singleUserModel = snapshot.data!; 87 | final lastMessage = lastSeenMessage(singleUserModel.lastSeen); 88 | 89 | return Text( 90 | singleUserModel.active ? 'online' : "last seen $lastMessage ago", 91 | style: const TextStyle( 92 | fontSize: 12, 93 | color: Colors.white, 94 | ), 95 | ); 96 | }, 97 | ), 98 | ], 99 | ), 100 | ), 101 | ), 102 | actions: [ 103 | CustomIconButton( 104 | onPressed: () {}, 105 | icon: Icons.video_call, 106 | iconColor: Colors.white, 107 | ), 108 | CustomIconButton( 109 | onPressed: () {}, 110 | icon: Icons.call, 111 | iconColor: Colors.white, 112 | ), 113 | CustomIconButton( 114 | onPressed: () {}, 115 | icon: Icons.more_vert, 116 | iconColor: Colors.white, 117 | ), 118 | ], 119 | ), 120 | body: Stack( 121 | children: [ 122 | // chat background image 123 | Image( 124 | height: double.maxFinite, 125 | width: double.maxFinite, 126 | image: const AssetImage('assets/images/doodle_bg.png'), 127 | fit: BoxFit.cover, 128 | color: context.theme.chatPageDoodleColor, 129 | ), 130 | // Stream of Chat 131 | Padding( 132 | padding: const EdgeInsets.only(bottom: 60), 133 | child: StreamBuilder( 134 | stream: ref.watch(chatControllerProvider).getAllOneToOneMessage(user.uid), 135 | builder: (context, snapshot) { 136 | if (snapshot.connectionState != ConnectionState.active) { 137 | return ListView.builder( 138 | itemCount: 15, 139 | itemBuilder: (_, index) { 140 | final random = Random().nextInt(14); 141 | return Container( 142 | alignment: random.isEven ? Alignment.centerRight : Alignment.centerLeft, 143 | margin: EdgeInsets.only( 144 | top: 5, 145 | bottom: 5, 146 | left: random.isEven ? 150 : 15, 147 | right: random.isEven ? 15 : 150, 148 | ), 149 | child: ClipPath( 150 | clipper: UpperNipMessageClipperTwo( 151 | random.isEven ? MessageType.send : MessageType.receive, 152 | nipWidth: 8, 153 | nipHeight: 10, 154 | bubbleRadius: 12, 155 | ), 156 | child: Shimmer.fromColors( 157 | baseColor: random.isEven 158 | ? context.theme.greyColor!.withOpacity(.3) 159 | : context.theme.greyColor!.withOpacity(.2), 160 | highlightColor: random.isEven 161 | ? context.theme.greyColor!.withOpacity(.4) 162 | : context.theme.greyColor!.withOpacity(.3), 163 | child: Container( 164 | height: 40, 165 | width: 170 + 166 | double.parse( 167 | (random * 2).toString(), 168 | ), 169 | color: Colors.red, 170 | ), 171 | ), 172 | ), 173 | ); 174 | }, 175 | ); 176 | } 177 | 178 | return PageStorage( 179 | bucket: pageStorageBucket, 180 | child: ListView.builder( 181 | key: const PageStorageKey('chat_page_list'), 182 | itemCount: snapshot.data!.length, 183 | shrinkWrap: true, 184 | controller: scrollController, 185 | itemBuilder: (_, index) { 186 | final message = snapshot.data![index]; 187 | final isSender = message.senderId == FirebaseAuth.instance.currentUser!.uid; 188 | 189 | final haveNip = (index == 0) || 190 | (index == snapshot.data!.length - 1 && 191 | message.senderId != snapshot.data![index - 1].senderId) || 192 | (message.senderId != snapshot.data![index - 1].senderId && 193 | message.senderId == snapshot.data![index + 1].senderId) || 194 | (message.senderId != snapshot.data![index - 1].senderId && 195 | message.senderId != snapshot.data![index + 1].senderId); 196 | final isShowDateCard = (index == 0) || 197 | ((index == snapshot.data!.length - 1) && 198 | (message.timeSent.day > snapshot.data![index - 1].timeSent.day)) || 199 | (message.timeSent.day > snapshot.data![index - 1].timeSent.day && 200 | message.timeSent.day <= snapshot.data![index + 1].timeSent.day); 201 | 202 | return Column( 203 | children: [ 204 | if (index == 0) const YellowCard(), 205 | if (isShowDateCard) ShowDateCard(date: message.timeSent), 206 | MessageCard( 207 | isSender: isSender, 208 | haveNip: haveNip, 209 | message: message, 210 | ), 211 | ], 212 | ); 213 | }, 214 | ), 215 | ); 216 | }, 217 | ), 218 | ), 219 | Container( 220 | alignment: const Alignment(0, 1), 221 | child: ChatTextField( 222 | receiverId: user.uid, 223 | scrollController: scrollController, 224 | ), 225 | ), 226 | ], 227 | ), 228 | ); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /lib/feature/chat/widgets/chat_text_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 | import 'package:whatsapp_messenger/common/enum/message_type.dart'; 5 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 6 | import 'package:whatsapp_messenger/common/utils/coloors.dart'; 7 | import 'package:whatsapp_messenger/common/widgets/custom_icon_button.dart'; 8 | import 'package:whatsapp_messenger/feature/auth/pages/image_picker_page.dart'; 9 | import 'package:whatsapp_messenger/feature/chat/controller/chat_controller.dart'; 10 | 11 | class ChatTextField extends ConsumerStatefulWidget { 12 | const ChatTextField({ 13 | super.key, 14 | required this.receiverId, 15 | required this.scrollController, 16 | }); 17 | 18 | final String receiverId; 19 | final ScrollController scrollController; 20 | 21 | @override 22 | ConsumerState createState() => _ChatTextFieldState(); 23 | } 24 | 25 | class _ChatTextFieldState extends ConsumerState { 26 | late TextEditingController messageController; 27 | 28 | bool isMessageIconEnabled = false; 29 | double cardHeight = 0; 30 | 31 | void sendImageMessageFromGallery() async { 32 | final image = await Navigator.push( 33 | context, 34 | MaterialPageRoute( 35 | builder: (_) => const ImagePickerPage(), 36 | )); 37 | 38 | if (image != null) { 39 | sendFileMessage(image, MessageType.image); 40 | setState(() => cardHeight = 0); 41 | } 42 | } 43 | 44 | void sendFileMessage(var file, MessageType messageType) async { 45 | ref.read(chatControllerProvider).sendFileMessage( 46 | context, 47 | file, 48 | widget.receiverId, 49 | messageType, 50 | ); 51 | await Future.delayed(const Duration(milliseconds: 500)); 52 | SchedulerBinding.instance.addPostFrameCallback((timeStamp) { 53 | widget.scrollController.animateTo( 54 | widget.scrollController.position.maxScrollExtent, 55 | duration: const Duration(milliseconds: 300), 56 | curve: Curves.easeOut, 57 | ); 58 | }); 59 | } 60 | 61 | void sendTextMessage() async { 62 | if (isMessageIconEnabled) { 63 | ref.read(chatControllerProvider).sendTextMessage( 64 | context: context, 65 | textMessage: messageController.text, 66 | receiverId: widget.receiverId, 67 | ); 68 | messageController.clear(); 69 | } 70 | 71 | await Future.delayed(const Duration(milliseconds: 100)); 72 | SchedulerBinding.instance.addPostFrameCallback((timeStamp) { 73 | widget.scrollController.animateTo( 74 | widget.scrollController.position.maxScrollExtent, 75 | duration: const Duration(milliseconds: 300), 76 | curve: Curves.easeOut, 77 | ); 78 | }); 79 | } 80 | 81 | iconWithText({ 82 | required VoidCallback onPressed, 83 | required IconData icon, 84 | required String text, 85 | required Color background, 86 | }) { 87 | return Column( 88 | children: [ 89 | CustomIconButton( 90 | onPressed: onPressed, 91 | icon: icon, 92 | background: background, 93 | minWidth: 50, 94 | iconColor: Colors.white, 95 | border: Border.all( 96 | color: context.theme.greyColor!.withOpacity(.2), 97 | width: 1, 98 | ), 99 | ), 100 | const SizedBox(height: 5), 101 | Text( 102 | text, 103 | style: TextStyle( 104 | color: context.theme.greyColor, 105 | ), 106 | ), 107 | ], 108 | ); 109 | } 110 | 111 | @override 112 | void initState() { 113 | messageController = TextEditingController(); 114 | super.initState(); 115 | } 116 | 117 | @override 118 | void dispose() { 119 | messageController.dispose(); 120 | super.dispose(); 121 | } 122 | 123 | @override 124 | Widget build(BuildContext context) { 125 | return Column( 126 | mainAxisSize: MainAxisSize.min, 127 | children: [ 128 | AnimatedContainer( 129 | duration: const Duration(milliseconds: 200), 130 | height: cardHeight, 131 | width: double.maxFinite, 132 | margin: const EdgeInsets.symmetric(horizontal: 10), 133 | decoration: BoxDecoration( 134 | color: context.theme.receiverChatCardBg, 135 | borderRadius: BorderRadius.circular(20), 136 | ), 137 | child: Center( 138 | child: SingleChildScrollView( 139 | child: Column( 140 | children: [ 141 | Row( 142 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 143 | children: [ 144 | iconWithText( 145 | onPressed: () {}, 146 | icon: Icons.book, 147 | text: 'File', 148 | background: const Color(0xFF7F66FE), 149 | ), 150 | iconWithText( 151 | onPressed: () {}, 152 | icon: Icons.camera_alt, 153 | text: 'Camera', 154 | background: const Color(0xFFFE2E74), 155 | ), 156 | iconWithText( 157 | onPressed: sendImageMessageFromGallery, 158 | icon: Icons.photo, 159 | text: 'Gallery', 160 | background: const Color(0xFFC861F9), 161 | ), 162 | ], 163 | ), 164 | const SizedBox(height: 20), 165 | Row( 166 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 167 | children: [ 168 | iconWithText( 169 | onPressed: () {}, 170 | icon: Icons.headphones, 171 | text: 'Audio', 172 | background: const Color(0xFFF96533), 173 | ), 174 | iconWithText( 175 | onPressed: () {}, 176 | icon: Icons.location_on, 177 | text: 'Location', 178 | background: const Color(0xFF1FA855), 179 | ), 180 | iconWithText( 181 | onPressed: () {}, 182 | icon: Icons.person, 183 | text: 'Contact', 184 | background: const Color(0xFF009DE1), 185 | ), 186 | ], 187 | ), 188 | ], 189 | ), 190 | ), 191 | ), 192 | ), 193 | Padding( 194 | padding: const EdgeInsets.all(5.0), 195 | child: Row( 196 | children: [ 197 | Expanded( 198 | child: TextFormField( 199 | controller: messageController, 200 | maxLines: 4, 201 | minLines: 1, 202 | onChanged: (value) { 203 | value.isEmpty 204 | ? setState(() => isMessageIconEnabled = false) 205 | : setState(() => isMessageIconEnabled = true); 206 | }, 207 | decoration: InputDecoration( 208 | hintText: 'Message', 209 | hintStyle: TextStyle(color: context.theme.greyColor), 210 | filled: true, 211 | fillColor: context.theme.chatTextFieldBg, 212 | isDense: true, 213 | border: OutlineInputBorder( 214 | borderSide: const BorderSide( 215 | style: BorderStyle.none, 216 | width: 0, 217 | ), 218 | borderRadius: BorderRadius.circular(30), 219 | ), 220 | prefixIcon: Material( 221 | color: Colors.transparent, 222 | child: CustomIconButton( 223 | onPressed: () {}, 224 | icon: Icons.emoji_emotions_outlined, 225 | iconColor: Theme.of(context).listTileTheme.iconColor, 226 | ), 227 | ), 228 | suffixIcon: Row( 229 | mainAxisSize: MainAxisSize.min, 230 | children: [ 231 | RotatedBox( 232 | quarterTurns: 45, 233 | child: CustomIconButton( 234 | onPressed: () => setState( 235 | () => cardHeight == 0 ? cardHeight = 220 : cardHeight = 0, 236 | ), 237 | icon: cardHeight == 0 ? Icons.attach_file : Icons.close, 238 | iconColor: Theme.of(context).listTileTheme.iconColor, 239 | ), 240 | ), 241 | CustomIconButton( 242 | onPressed: () {}, 243 | icon: Icons.camera_alt_outlined, 244 | iconColor: Theme.of(context).listTileTheme.iconColor, 245 | ), 246 | ], 247 | ), 248 | ), 249 | ), 250 | ), 251 | const SizedBox(width: 5), 252 | CustomIconButton( 253 | onPressed: sendTextMessage, 254 | icon: isMessageIconEnabled ? Icons.send_outlined : Icons.mic_none_outlined, 255 | background: Coloors.greenDark, 256 | iconColor: Colors.white, 257 | ), 258 | ], 259 | ), 260 | ), 261 | ], 262 | ); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /lib/feature/chat/pages/profile_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:whatsapp_messenger/common/extension/custom_theme_extension.dart'; 4 | import 'package:whatsapp_messenger/common/helper/last_seen_message.dart'; 5 | import 'package:whatsapp_messenger/common/models/user_model.dart'; 6 | import 'package:whatsapp_messenger/common/utils/coloors.dart'; 7 | import 'package:whatsapp_messenger/common/widgets/custom_icon_button.dart'; 8 | import 'package:whatsapp_messenger/feature/chat/widgets/custom_list_tile.dart'; 9 | 10 | class ProfilePage extends StatelessWidget { 11 | const ProfilePage({super.key, required this.user}); 12 | 13 | final UserModel user; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Scaffold( 18 | backgroundColor: context.theme.profilePageBg, 19 | body: CustomScrollView( 20 | slivers: [ 21 | SliverPersistentHeader( 22 | delegate: SliverPersistentDelegate(user), 23 | pinned: true, 24 | ), 25 | // lets create a long list to make the content scrollable 26 | SliverToBoxAdapter( 27 | child: Column( 28 | children: [ 29 | Container( 30 | color: Theme.of(context).backgroundColor, 31 | child: Column( 32 | children: [ 33 | Text( 34 | user.username, 35 | style: const TextStyle(fontSize: 24), 36 | ), 37 | const SizedBox(height: 10), 38 | Text( 39 | user.phoneNumber, 40 | style: TextStyle( 41 | fontSize: 20, 42 | color: context.theme.greyColor, 43 | ), 44 | ), 45 | const SizedBox(height: 10), 46 | Text( 47 | "last seen ${lastSeenMessage(user.lastSeen)} ago", 48 | style: TextStyle(color: context.theme.greyColor), 49 | ), 50 | Row( 51 | mainAxisAlignment: MainAxisAlignment.center, 52 | children: [ 53 | iconWithText(icon: Icons.call, text: 'Call'), 54 | iconWithText(icon: Icons.video_call, text: 'Video'), 55 | iconWithText(icon: Icons.search, text: 'Search'), 56 | ], 57 | ), 58 | ], 59 | ), 60 | ), 61 | const SizedBox(height: 20), 62 | ListTile( 63 | contentPadding: const EdgeInsets.only(left: 30), 64 | title: const Text('Hey there! I am using WhatsApp'), 65 | subtitle: Text( 66 | '17th February', 67 | style: TextStyle( 68 | color: context.theme.greyColor, 69 | ), 70 | ), 71 | ), 72 | const SizedBox(height: 20), 73 | CustomListTile( 74 | title: 'Mute notification', 75 | leading: Icons.notifications, 76 | trailing: Switch( 77 | value: false, 78 | onChanged: (value) {}, 79 | ), 80 | ), 81 | const CustomListTile( 82 | title: 'Custom notification', 83 | leading: Icons.music_note, 84 | ), 85 | CustomListTile( 86 | title: 'Media visibility', 87 | leading: Icons.photo, 88 | trailing: Switch( 89 | value: false, 90 | onChanged: (value) {}, 91 | ), 92 | ), 93 | const SizedBox(height: 20), 94 | const CustomListTile( 95 | title: 'Encryption', 96 | subTitle: 97 | 'Messages and calls are end-to-end encrypted, Tap to verify.', 98 | leading: Icons.lock, 99 | ), 100 | const CustomListTile( 101 | title: 'Disappearing messages', 102 | subTitle: 'Off', 103 | leading: Icons.timer, 104 | ), 105 | const SizedBox(height: 20), 106 | ListTile( 107 | leading: CustomIconButton( 108 | onPressed: () {}, 109 | icon: Icons.group, 110 | background: Coloors.greenDark, 111 | iconColor: Colors.white, 112 | ), 113 | title: Text('Create group with ${user.username}'), 114 | ), 115 | const SizedBox(height: 20), 116 | ListTile( 117 | contentPadding: const EdgeInsets.only(left: 25, right: 10), 118 | leading: const Icon( 119 | Icons.block, 120 | color: Color(0xFFF15C6D), 121 | ), 122 | title: Text( 123 | 'Block ${user.username}', 124 | style: const TextStyle( 125 | color: Color(0xFFF15C6D), 126 | ), 127 | ), 128 | ), 129 | ListTile( 130 | contentPadding: const EdgeInsets.only(left: 25, right: 10), 131 | leading: const Icon( 132 | Icons.thumb_down, 133 | color: Color(0xFFF15C6D), 134 | ), 135 | title: Text( 136 | 'Report ${user.username}', 137 | style: const TextStyle( 138 | color: Color(0xFFF15C6D), 139 | ), 140 | ), 141 | ), 142 | const SizedBox(height: 40), 143 | ], 144 | ), 145 | ), 146 | ], 147 | ), 148 | ); 149 | } 150 | 151 | iconWithText({required IconData icon, required String text}) { 152 | return Padding( 153 | padding: const EdgeInsets.all(20), 154 | child: Column( 155 | mainAxisSize: MainAxisSize.min, 156 | children: [ 157 | Icon( 158 | icon, 159 | size: 30, 160 | color: Coloors.greenDark, 161 | ), 162 | const SizedBox(height: 10), 163 | Text( 164 | text, 165 | style: const TextStyle(color: Coloors.greenDark), 166 | ), 167 | ], 168 | ), 169 | ); 170 | } 171 | } 172 | 173 | class SliverPersistentDelegate extends SliverPersistentHeaderDelegate { 174 | final UserModel user; 175 | 176 | final double maxHeaderHeight = 180; 177 | final double minHeaderHeight = kToolbarHeight + 20; 178 | final double maxImageSize = 130; 179 | final double minImageSize = 40; 180 | 181 | SliverPersistentDelegate(this.user); 182 | 183 | @override 184 | Widget build( 185 | BuildContext context, 186 | double shrinkOffset, 187 | bool overlapsContent, 188 | ) { 189 | final size = MediaQuery.of(context).size; 190 | final percent = shrinkOffset / (maxHeaderHeight - 35); 191 | final percent2 = shrinkOffset / (maxHeaderHeight); 192 | final currentImageSize = (maxImageSize * (1 - percent)).clamp( 193 | minImageSize, 194 | maxImageSize, 195 | ); 196 | final currentImagePosition = ((size.width / 2 - 65) * (1 - percent)).clamp( 197 | minImageSize, 198 | maxImageSize, 199 | ); 200 | return Container( 201 | color: Theme.of(context).backgroundColor, 202 | child: Container( 203 | color: Theme.of(context) 204 | .appBarTheme 205 | .backgroundColor! 206 | .withOpacity(percent2 * 2 < 1 ? percent2 * 2 : 1), 207 | child: Stack( 208 | children: [ 209 | Positioned( 210 | top: MediaQuery.of(context).viewPadding.top + 15, 211 | left: currentImagePosition + 50, 212 | child: Text( 213 | user.username, 214 | style: TextStyle( 215 | fontSize: 20, 216 | color: Colors.white.withOpacity(percent2), 217 | ), 218 | ), 219 | ), 220 | Positioned( 221 | left: 0, 222 | top: MediaQuery.of(context).viewPadding.top + 5, 223 | child: BackButton( 224 | color: 225 | percent2 > .3 ? Colors.white.withOpacity(percent2) : null, 226 | ), 227 | ), 228 | Positioned( 229 | right: 0, 230 | top: MediaQuery.of(context).viewPadding.top + 5, 231 | child: CustomIconButton( 232 | onPressed: () {}, 233 | icon: Icons.more_vert, 234 | iconColor: percent2 > .3 235 | ? Colors.white.withOpacity(percent2) 236 | : Theme.of(context).textTheme.bodyText2!.color, 237 | ), 238 | ), 239 | Positioned( 240 | left: currentImagePosition, 241 | top: MediaQuery.of(context).viewPadding.top + 5, 242 | bottom: 0, 243 | child: Hero( 244 | tag: 'profile', 245 | child: Container( 246 | width: currentImageSize, 247 | decoration: BoxDecoration( 248 | shape: BoxShape.circle, 249 | image: DecorationImage( 250 | image: CachedNetworkImageProvider(user.profileImageUrl), 251 | ), 252 | ), 253 | ), 254 | ), 255 | ), 256 | ], 257 | ), 258 | ), 259 | ); 260 | } 261 | 262 | @override 263 | double get maxExtent => maxHeaderHeight; 264 | 265 | @override 266 | double get minExtent => minHeaderHeight; 267 | 268 | @override 269 | bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { 270 | return false; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 94 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 95 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 96 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 97 | ); 98 | path = Runner; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | 97C146ED1CF9000F007C117D /* Runner */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 107 | buildPhases = ( 108 | 9740EEB61CF901F6004384FC /* Run Script */, 109 | 97C146EA1CF9000F007C117D /* Sources */, 110 | 97C146EB1CF9000F007C117D /* Frameworks */, 111 | 97C146EC1CF9000F007C117D /* Resources */, 112 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 113 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = Runner; 120 | productName = Runner; 121 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | /* End PBXNativeTarget section */ 125 | 126 | /* Begin PBXProject section */ 127 | 97C146E61CF9000F007C117D /* Project object */ = { 128 | isa = PBXProject; 129 | attributes = { 130 | LastUpgradeCheck = 1300; 131 | ORGANIZATIONNAME = ""; 132 | TargetAttributes = { 133 | 97C146ED1CF9000F007C117D = { 134 | CreatedOnToolsVersion = 7.3.1; 135 | LastSwiftMigration = 1100; 136 | }; 137 | }; 138 | }; 139 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 140 | compatibilityVersion = "Xcode 9.3"; 141 | developmentRegion = en; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | Base, 146 | ); 147 | mainGroup = 97C146E51CF9000F007C117D; 148 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 149 | projectDirPath = ""; 150 | projectRoot = ""; 151 | targets = ( 152 | 97C146ED1CF9000F007C117D /* Runner */, 153 | ); 154 | }; 155 | /* End PBXProject section */ 156 | 157 | /* Begin PBXResourcesBuildPhase section */ 158 | 97C146EC1CF9000F007C117D /* Resources */ = { 159 | isa = PBXResourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 163 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 164 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 165 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXShellScriptBuildPhase section */ 172 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 173 | isa = PBXShellScriptBuildPhase; 174 | buildActionMask = 2147483647; 175 | files = ( 176 | ); 177 | inputPaths = ( 178 | ); 179 | name = "Thin Binary"; 180 | outputPaths = ( 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | shellPath = /bin/sh; 184 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 185 | }; 186 | 9740EEB61CF901F6004384FC /* Run Script */ = { 187 | isa = PBXShellScriptBuildPhase; 188 | buildActionMask = 2147483647; 189 | files = ( 190 | ); 191 | inputPaths = ( 192 | ); 193 | name = "Run Script"; 194 | outputPaths = ( 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | shellPath = /bin/sh; 198 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 199 | }; 200 | /* End PBXShellScriptBuildPhase section */ 201 | 202 | /* Begin PBXSourcesBuildPhase section */ 203 | 97C146EA1CF9000F007C117D /* Sources */ = { 204 | isa = PBXSourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 208 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 209 | ); 210 | runOnlyForDeploymentPostprocessing = 0; 211 | }; 212 | /* End PBXSourcesBuildPhase section */ 213 | 214 | /* Begin PBXVariantGroup section */ 215 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 216 | isa = PBXVariantGroup; 217 | children = ( 218 | 97C146FB1CF9000F007C117D /* Base */, 219 | ); 220 | name = Main.storyboard; 221 | sourceTree = ""; 222 | }; 223 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 224 | isa = PBXVariantGroup; 225 | children = ( 226 | 97C147001CF9000F007C117D /* Base */, 227 | ); 228 | name = LaunchScreen.storyboard; 229 | sourceTree = ""; 230 | }; 231 | /* End PBXVariantGroup section */ 232 | 233 | /* Begin XCBuildConfiguration section */ 234 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 235 | isa = XCBuildConfiguration; 236 | buildSettings = { 237 | ALWAYS_SEARCH_USER_PATHS = NO; 238 | CLANG_ANALYZER_NONNULL = YES; 239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 240 | CLANG_CXX_LIBRARY = "libc++"; 241 | CLANG_ENABLE_MODULES = YES; 242 | CLANG_ENABLE_OBJC_ARC = YES; 243 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 244 | CLANG_WARN_BOOL_CONVERSION = YES; 245 | CLANG_WARN_COMMA = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNREACHABLE_CODE = YES; 261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 262 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 263 | COPY_PHASE_STRIP = NO; 264 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 265 | ENABLE_NS_ASSERTIONS = NO; 266 | ENABLE_STRICT_OBJC_MSGSEND = YES; 267 | GCC_C_LANGUAGE_STANDARD = gnu99; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 276 | MTL_ENABLE_DEBUG_INFO = NO; 277 | SDKROOT = iphoneos; 278 | SUPPORTED_PLATFORMS = iphoneos; 279 | TARGETED_DEVICE_FAMILY = "1,2"; 280 | VALIDATE_PRODUCT = YES; 281 | }; 282 | name = Profile; 283 | }; 284 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 285 | isa = XCBuildConfiguration; 286 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 287 | buildSettings = { 288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 289 | CLANG_ENABLE_MODULES = YES; 290 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 291 | ENABLE_BITCODE = NO; 292 | INFOPLIST_FILE = Runner/Info.plist; 293 | LD_RUNPATH_SEARCH_PATHS = ( 294 | "$(inherited)", 295 | "@executable_path/Frameworks", 296 | ); 297 | PRODUCT_BUNDLE_IDENTIFIER = com.example.whatsappMessenger; 298 | PRODUCT_NAME = "$(TARGET_NAME)"; 299 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 300 | SWIFT_VERSION = 5.0; 301 | VERSIONING_SYSTEM = "apple-generic"; 302 | }; 303 | name = Profile; 304 | }; 305 | 97C147031CF9000F007C117D /* Debug */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ALWAYS_SEARCH_USER_PATHS = NO; 309 | CLANG_ANALYZER_NONNULL = YES; 310 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 311 | CLANG_CXX_LIBRARY = "libc++"; 312 | CLANG_ENABLE_MODULES = YES; 313 | CLANG_ENABLE_OBJC_ARC = YES; 314 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 315 | CLANG_WARN_BOOL_CONVERSION = YES; 316 | CLANG_WARN_COMMA = YES; 317 | CLANG_WARN_CONSTANT_CONVERSION = YES; 318 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 319 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 320 | CLANG_WARN_EMPTY_BODY = YES; 321 | CLANG_WARN_ENUM_CONVERSION = YES; 322 | CLANG_WARN_INFINITE_RECURSION = YES; 323 | CLANG_WARN_INT_CONVERSION = YES; 324 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 325 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 326 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 328 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 329 | CLANG_WARN_STRICT_PROTOTYPES = YES; 330 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 331 | CLANG_WARN_UNREACHABLE_CODE = YES; 332 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 333 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 334 | COPY_PHASE_STRIP = NO; 335 | DEBUG_INFORMATION_FORMAT = dwarf; 336 | ENABLE_STRICT_OBJC_MSGSEND = YES; 337 | ENABLE_TESTABILITY = YES; 338 | GCC_C_LANGUAGE_STANDARD = gnu99; 339 | GCC_DYNAMIC_NO_PIC = NO; 340 | GCC_NO_COMMON_BLOCKS = YES; 341 | GCC_OPTIMIZATION_LEVEL = 0; 342 | GCC_PREPROCESSOR_DEFINITIONS = ( 343 | "DEBUG=1", 344 | "$(inherited)", 345 | ); 346 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 347 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 348 | GCC_WARN_UNDECLARED_SELECTOR = YES; 349 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 350 | GCC_WARN_UNUSED_FUNCTION = YES; 351 | GCC_WARN_UNUSED_VARIABLE = YES; 352 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 353 | MTL_ENABLE_DEBUG_INFO = YES; 354 | ONLY_ACTIVE_ARCH = YES; 355 | SDKROOT = iphoneos; 356 | TARGETED_DEVICE_FAMILY = "1,2"; 357 | }; 358 | name = Debug; 359 | }; 360 | 97C147041CF9000F007C117D /* Release */ = { 361 | isa = XCBuildConfiguration; 362 | buildSettings = { 363 | ALWAYS_SEARCH_USER_PATHS = NO; 364 | CLANG_ANALYZER_NONNULL = YES; 365 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 366 | CLANG_CXX_LIBRARY = "libc++"; 367 | CLANG_ENABLE_MODULES = YES; 368 | CLANG_ENABLE_OBJC_ARC = YES; 369 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 370 | CLANG_WARN_BOOL_CONVERSION = YES; 371 | CLANG_WARN_COMMA = YES; 372 | CLANG_WARN_CONSTANT_CONVERSION = YES; 373 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 374 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 375 | CLANG_WARN_EMPTY_BODY = YES; 376 | CLANG_WARN_ENUM_CONVERSION = YES; 377 | CLANG_WARN_INFINITE_RECURSION = YES; 378 | CLANG_WARN_INT_CONVERSION = YES; 379 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 380 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 381 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 382 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 383 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 384 | CLANG_WARN_STRICT_PROTOTYPES = YES; 385 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 386 | CLANG_WARN_UNREACHABLE_CODE = YES; 387 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 388 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 389 | COPY_PHASE_STRIP = NO; 390 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 391 | ENABLE_NS_ASSERTIONS = NO; 392 | ENABLE_STRICT_OBJC_MSGSEND = YES; 393 | GCC_C_LANGUAGE_STANDARD = gnu99; 394 | GCC_NO_COMMON_BLOCKS = YES; 395 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 396 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 397 | GCC_WARN_UNDECLARED_SELECTOR = YES; 398 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 399 | GCC_WARN_UNUSED_FUNCTION = YES; 400 | GCC_WARN_UNUSED_VARIABLE = YES; 401 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 402 | MTL_ENABLE_DEBUG_INFO = NO; 403 | SDKROOT = iphoneos; 404 | SUPPORTED_PLATFORMS = iphoneos; 405 | SWIFT_COMPILATION_MODE = wholemodule; 406 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 407 | TARGETED_DEVICE_FAMILY = "1,2"; 408 | VALIDATE_PRODUCT = YES; 409 | }; 410 | name = Release; 411 | }; 412 | 97C147061CF9000F007C117D /* Debug */ = { 413 | isa = XCBuildConfiguration; 414 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 415 | buildSettings = { 416 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 417 | CLANG_ENABLE_MODULES = YES; 418 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 419 | ENABLE_BITCODE = NO; 420 | INFOPLIST_FILE = Runner/Info.plist; 421 | LD_RUNPATH_SEARCH_PATHS = ( 422 | "$(inherited)", 423 | "@executable_path/Frameworks", 424 | ); 425 | PRODUCT_BUNDLE_IDENTIFIER = com.example.whatsappMessenger; 426 | PRODUCT_NAME = "$(TARGET_NAME)"; 427 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 428 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 429 | SWIFT_VERSION = 5.0; 430 | VERSIONING_SYSTEM = "apple-generic"; 431 | }; 432 | name = Debug; 433 | }; 434 | 97C147071CF9000F007C117D /* Release */ = { 435 | isa = XCBuildConfiguration; 436 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 437 | buildSettings = { 438 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 439 | CLANG_ENABLE_MODULES = YES; 440 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 441 | ENABLE_BITCODE = NO; 442 | INFOPLIST_FILE = Runner/Info.plist; 443 | LD_RUNPATH_SEARCH_PATHS = ( 444 | "$(inherited)", 445 | "@executable_path/Frameworks", 446 | ); 447 | PRODUCT_BUNDLE_IDENTIFIER = com.example.whatsappMessenger; 448 | PRODUCT_NAME = "$(TARGET_NAME)"; 449 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 450 | SWIFT_VERSION = 5.0; 451 | VERSIONING_SYSTEM = "apple-generic"; 452 | }; 453 | name = Release; 454 | }; 455 | /* End XCBuildConfiguration section */ 456 | 457 | /* Begin XCConfigurationList section */ 458 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 459 | isa = XCConfigurationList; 460 | buildConfigurations = ( 461 | 97C147031CF9000F007C117D /* Debug */, 462 | 97C147041CF9000F007C117D /* Release */, 463 | 249021D3217E4FDB00AE95B9 /* Profile */, 464 | ); 465 | defaultConfigurationIsVisible = 0; 466 | defaultConfigurationName = Release; 467 | }; 468 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 469 | isa = XCConfigurationList; 470 | buildConfigurations = ( 471 | 97C147061CF9000F007C117D /* Debug */, 472 | 97C147071CF9000F007C117D /* Release */, 473 | 249021D4217E4FDB00AE95B9 /* Profile */, 474 | ); 475 | defaultConfigurationIsVisible = 0; 476 | defaultConfigurationName = Release; 477 | }; 478 | /* End XCConfigurationList section */ 479 | }; 480 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 481 | } 482 | --------------------------------------------------------------------------------