├── .gitignore ├── .gradle ├── 5.1.1 │ ├── fileChanges │ │ └── last-build.bin │ ├── fileHashes │ │ └── fileHashes.lock │ └── gc.properties ├── buildOutputCleanup │ ├── buildOutputCleanup.lock │ └── cache.properties └── vcs-1 │ └── gc.properties ├── .metadata ├── .travis.yml ├── README.md ├── android ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ ├── com │ │ │ │ └── app │ │ │ │ │ └── messio │ │ │ │ │ └── messio │ │ │ │ │ └── MainActivity.java │ │ │ └── io │ │ │ │ └── flutter │ │ │ │ └── plugins │ │ │ │ └── GeneratedPluginRegistrant.java │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-mdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xhdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxhdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable-xxxhdpi │ │ │ ├── ic_launcher_background.png │ │ │ └── ic_launcher_foreground.png │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ └── ic_launcher.xml │ │ │ ├── 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 │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties └── settings.gradle ├── assets ├── fonts │ ├── manrope-bold.otf │ ├── manrope-extrabold.otf │ ├── manrope-light.otf │ ├── manrope-medium.otf │ ├── manrope-regular.otf │ ├── manrope-semibold.otf │ └── manrope-thin.otf ├── google.png ├── launcher │ ├── ic_background.png │ ├── ic_foreground.png │ └── ic_launcher.png ├── placeholder.png ├── social.png └── user.png ├── howModalBottomSheet ├── lib ├── blocs │ ├── attachments │ │ ├── attachments_bloc.dart │ │ ├── attachments_event.dart │ │ ├── attachments_state.dart │ │ └── bloc.dart │ ├── authentication │ │ ├── authentication_bloc.dart │ │ ├── authentication_event.dart │ │ ├── authentication_state.dart │ │ └── bloc.dart │ ├── chats │ │ ├── bloc.dart │ │ ├── chat_bloc.dart │ │ ├── chat_event.dart │ │ └── chat_state.dart │ ├── config │ │ ├── bloc.dart │ │ ├── config_bloc.dart │ │ ├── config_event.dart │ │ └── config_state.dart │ ├── contacts │ │ ├── bloc.dart │ │ ├── contacts_bloc.dart │ │ ├── contacts_event.dart │ │ └── contacts_state.dart │ └── home │ │ ├── bloc.dart │ │ ├── home_bloc.dart │ │ ├── home_event.dart │ │ └── home_state.dart ├── config │ ├── assets.dart │ ├── constants.dart │ ├── decorations.dart │ ├── palette.dart │ ├── paths.dart │ ├── styles.dart │ ├── themes.dart │ └── transitions.dart ├── main.dart ├── models │ ├── chat.dart │ ├── contact.dart │ ├── conversation.dart │ ├── message.dart │ ├── messio_user.dart │ └── video_wrapper.dart ├── pages │ ├── attachment_page.dart │ ├── contact_list_page.dart │ ├── conversation_page.dart │ ├── conversation_page_slide.dart │ ├── home_page.dart │ ├── register_page.dart │ ├── settings_page.dart │ └── single_conversation_page.dart ├── providers │ ├── authentication_provider.dart │ ├── base_providers.dart │ ├── chat_provider.dart │ ├── storage_provider.dart │ └── user_data_provider.dart ├── repositories │ ├── authentication_repository.dart │ ├── base_repository.dart │ ├── chat_repository.dart │ ├── storage_repository.dart │ └── user_data_repository.dart ├── utils │ ├── document_snapshot_extension.dart │ ├── exceptions.dart │ ├── shared_objects.dart │ ├── validators.dart │ └── video_thumbnail.dart └── widgets │ ├── bottom_sheet_fixed.dart │ ├── chat_app_bar.dart │ ├── chat_item_widget.dart │ ├── chat_list_widget.dart │ ├── chat_row_widget.dart │ ├── circle_indicator.dart │ ├── contact_row_widget.dart │ ├── conversation_bottom_sheet.dart │ ├── conversation_list_widget.dart │ ├── gradient_fab.dart │ ├── gradient_snack_bar.dart │ ├── image_full_screen_widget.dart │ ├── input_widget.dart │ ├── navigation_pill_widget.dart │ ├── number_picker.dart │ ├── quick_scroll_bar.dart │ └── video_player_widget.dart ├── pubspec.lock ├── pubspec.yaml ├── server ├── .gitignore ├── firebase.json ├── firestore.indexes.json ├── firestore.rules └── functions │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── src │ └── index.ts │ ├── tsconfig.json │ └── tslint.json └── test ├── blocs └── authentication_bloc_test.dart ├── main_test.dart ├── mock ├── data_mock.dart ├── firebase_mock.dart ├── io_mock.dart ├── repository_mock.dart └── shared_objects_mock.dart ├── pages ├── conversation_page_slide_test.dart └── conversation_page_test.dart ├── providers ├── authentication_provider_test.dart ├── chat_provider_test.dart ├── storage_provider_test.dart └── user_data_provider_test.dart └── widgets ├── chat_app_bar_test.dart ├── chat_item_widget_test.dart ├── chat_list_widget_test.dart └── input_widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .flutter-plugins-dependencies 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 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 | .dart_tool/ 27 | .flutter-plugins 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | #Platform specific 34 | /android/app/google-services.json 35 | /android/**/gradle-wrapper.jar 36 | /android/.gradle 37 | /android/captures/ 38 | /android/gradlew 39 | /android/gradlew.bat 40 | /android/local.properties 41 | /android/**/GeneratedPluginRegistrant.java 42 | /android/keystore.properties 43 | /ios/ 44 | 45 | #Firebase setup related 46 | /server/.firebaserc 47 | /android/.gradle/ 48 | -------------------------------------------------------------------------------- /.gradle/5.1.1/fileChanges/last-build.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gradle/5.1.1/fileHashes/fileHashes.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/.gradle/5.1.1/fileHashes/fileHashes.lock -------------------------------------------------------------------------------- /.gradle/5.1.1/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/.gradle/5.1.1/gc.properties -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/buildOutputCleanup.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/.gradle/buildOutputCleanup/buildOutputCleanup.lock -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/cache.properties: -------------------------------------------------------------------------------- 1 | #Mon Aug 19 00:34:07 IST 2019 2 | gradle.version=5.1.1 3 | -------------------------------------------------------------------------------- /.gradle/vcs-1/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/.gradle/vcs-1/gc.properties -------------------------------------------------------------------------------- /.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 and should not be manually edited. 5 | 6 | version: 7 | revision: b712a172f9694745f50505c93340883493b505e5 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | dist: bionic 3 | addons: 4 | apt: 5 | packages: 6 | - lib32stdc++6 7 | install: 8 | - git clone https://github.com/flutter/flutter.git -b stable --depth 1 9 | - export PATH=./flutter/bin:$PATH 10 | - flutter doctor 11 | - flutter --version 12 | script: 13 | - flutter packages get 14 | - flutter analyze --no-pub --no-current-package lib/ test/ 15 | - flutter test --no-pub test/ 16 | cache: 17 | directories: 18 | - $HOME/.pub-cache 19 | - $HOME/.pub-cache -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![Messio](https://raw.githubusercontent.com/adityadroid/Messio/master/assets/launcher/ic_launcher.png) 3 | # Messio 4 | An open source messenger app built using flutter. 5 | 6 | Part of Medium Series [60 days of Flutter](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-from-scratch-ab2c89e1fd0f) written by [Aditya Gurjar](https://medium.com/@adityadroid) 7 | 8 | ### Posts In This Series 9 | 10 | [60 Days Of Flutter : Building a Messenger from Scratch](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-from-scratch-ab2c89e1fd0f) 11 | 12 | [60 Days of Flutter : Day 1 : Creating the App](https://medium.com/@adityadroid/60-days-of-flutter-creating-the-app-ea0407b472ae) 13 | 14 | [60 Days of Flutter : Day 2 : Setting Up A CI With Flutter](https://medium.com/@adityadroid/60-days-of-flutter-day-2-setting-up-a-ci-with-flutter-8f77bebd1b86) 15 | 16 | [60 Days of Flutter : Day 3–4 : Building a Chat Screen in Flutter](https://medium.com/@adityadroid/60-days-of-flutter-day-3-4-building-a-chat-screen-in-flutter-e2ed36388dc7?sk=da6cf57f149fa2d61d481cb2f93edde4) 17 | 18 | [60 Days of Flutter : Day 4–5 : Widget Testing With Flutter](https://medium.com/@adityadroid/60-days-of-flutter-day-4-5-widget-testing-with-flutter-a30236dd04fc?sk=50feba54f4cf238b2422f72ce3da01f0) 19 | 20 | [60 Days of Flutter : Day 6–7 : Implementing a Slideable Widget Using Bottomsheet in Flutter](https://medium.com/@adityadroid/60-days-of-flutter-day-6-7-implementing-a-slideable-widget-using-bottomsheet-in-flutter-c320dc106f2b?sk=18ddb63ed2fb69dfc90d6aa8f4ac574f) 21 | 22 | [60 Days of Flutter : Day 8 : Changing The Launcher Icon and Implementing GestureDetector](https://medium.com/@adityadroid/60-days-of-flutter-day-8-changing-the-launcher-icon-and-implementing-gesturedetector-421a00ad854a) 23 | 24 | [60 Days of Flutter : Day 9–10–11 : Creating Awesome Register Screen in Flutter](https://medium.com/@adityadroid/60-days-of-flutter-day-9-10-11-creating-awesome-register-screen-in-flutter-77db27227c07) 25 | 26 | [60 Days of Flutter : Day 12–14 : Understanding BLoC Pattern in Flutter](https://medium.com/@adityadroid/60-days-of-flutter-day-12-14-understanding-bloc-pattern-in-flutter-8703486f716d) 27 | 28 | [60 Days of Flutter : Day 15–17 : Implementing Registration Screen using ‘flutter_bloc’](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-day-15-17-implementing-registration-screen-using-d3a708d866a9) 29 | 30 | [60 Days of Flutter : Day 18–19 : Unit Testing in Flutter using ‘ mockito’](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-day-18-19-unit-testing-in-flutter-using-mockito-8bafd7445dd8) 31 | 32 | [60 Days of Flutter : Day 20–21 : Unit Testing a Bloc in Flutter](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-day-18-19-unit-testing-a-bloc-in-flutter-61e3c58918a2) 33 | 34 | [60 Days of Flutter : Day 22–23 : Building a Modern Contacts Page in Flutter](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-day-22-23-building-a-modern-contacts-page-in-flutter-9f3cfd01d08d) 35 | 36 | [60 Days of Flutter : Day 24–26 : Building a Animated Progress Fab and the Contacts Bloc in Flutter](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-day-24-26-building-a-animated-progress-fab-and-the-bf28c59b8472) 37 | 38 | [60 Days of Flutter : Day 27–29 : Sending and Retrieving Messages from Firebase using BLOC](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-day-27-29-sending-and-retrieving-messages-from-e44135c8e95f) 39 | 40 | [60 Days of Flutter : Day 30–32 : Firebase Chat UI using Stream and Bloc](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-day-30-32-firebase-chat-ui-using-stream-and-bloc-5d0e5f3e914b) 41 | 42 | [60 Days of Flutter : Day 33–35 : Paginating data from Firestore using Firebase Queries](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-day-33-35-paginating-data-from-firestore-using-2391b90e8cda) 43 | 44 | [60 Days of Flutter : Day 36–38 : Seamlessly Upload Files to Firebase Storage](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-day-36-38-chat-attachments-seamlessly-upload-and-8d334a4e52b5) 45 | 46 | [60 Days of Flutter : Day 39–41 : One UI Inspired Attachments Showcase Page](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-day-39-41-one-ui-inspired-attachments-showcase-page-5962af007ab4) 47 | 48 | [60 Days of Flutter : Day 42–44 : Creating the Home Page & Quick Peek BottomSheet for Messages](https://medium.com/@adityadroid/60-days-of-flutter-building-a-messenger-day-42-45-creating-the-home-page-quick-peek-7264bc68e86d) 49 | 50 | Happy learning. :+1: 51 | 52 | ### Show some :heart: and star the repo to support the project 53 | 54 | [![GitHub stars](https://img.shields.io/github/stars/adityadroid/messio.svg?style=social&label=Star)](https://github.com/adityadroid/messio) [![GitHub forks](https://img.shields.io/github/forks/adityadroid/messio.svg?style=social&label=Fork)](https://github.com/adityadroid/messio/fork) [![GitHub watchers](https://img.shields.io/github/watchers/adityadroid/messio.svg?style=social&label=Watch)](https://github.com/adityadroid/messio) [![GitHub followers](https://img.shields.io/github/followers/adityadroid.svg?style=social&label=Follow)](https://github.com/adityadroid/messio) 55 | [![Twitter Follow](https://img.shields.io/twitter/follow/adityadroid.svg?style=social)](https://twitter.com/adityadroid) 56 | 57 | 58 | 59 | ### :heart: Found this project useful? 60 | 61 | If you found this project useful, then please consider giving it a :star: on Github and sharing it with your friends via social media. 62 | 63 | ## Project Created & Maintained By 64 | 65 | ### Aditya Gurjar 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | # Donate 74 | 75 | > If you found this project helpful or you learned something from the source code and want to thank me, consider buying me a cup of :coffee: 76 | > 77 | > - [PayPal](https://www.paypal.me/adityadroid/) 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 from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.app.messio.messio" 37 | minSdkVersion 19 38 | targetSdkVersion 33 39 | compileSdkVersion 33 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 43 | multiDexEnabled true 44 | } 45 | 46 | buildTypes { 47 | release { 48 | // TODO: Add your own signing config for the release build. 49 | // Signing with the debug keys for now, so `flutter run --release` works. 50 | signingConfig signingConfigs.debug 51 | } 52 | debug { 53 | minifyEnabled false 54 | useProguard false 55 | } 56 | } 57 | } 58 | 59 | flutter { 60 | source '../..' 61 | } 62 | 63 | dependencies { 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'androidx.test:runner:1.2.0' 66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 67 | implementation 'androidx.multidex:multidex:2.0.1' 68 | 69 | } 70 | apply plugin: 'com.google.gms.google-services' // Google Play services Gradle plugin 71 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 13 | 21 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 42 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/app/messio/messio/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.app.messio.messio; 2 | 3 | import android.graphics.Bitmap; 4 | import android.media.MediaMetadataRetriever; 5 | import android.os.AsyncTask; 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | import java.io.IOException; 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.FileOutputStream; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | import io.flutter.plugins.GeneratedPluginRegistrant; 15 | import androidx.annotation.NonNull; 16 | import io.flutter.plugin.common.MethodChannel; 17 | import io.flutter.embedding.android.FlutterActivity; 18 | import io.flutter.embedding.engine.FlutterEngine; 19 | import io.flutter.plugins.GeneratedPluginRegistrant; 20 | 21 | public class MainActivity extends FlutterActivity { 22 | private static String TAG = "Android Platform"; 23 | private static final int HIGH_QUALITY_MIN_VAL = 70; 24 | @Override 25 | public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { 26 | GeneratedPluginRegistrant.registerWith(flutterEngine); 27 | new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "app.messio.channel").setMethodCallHandler( 28 | (call, result) -> { 29 | final Map args = call.arguments(); 30 | 31 | try { 32 | final String video = (String) args.get("video"); 33 | final int format = (int) args.get("format"); 34 | final int maxhow = (int) args.get("maxhow"); 35 | final int quality = (int) args.get("quality"); 36 | new Thread(() -> { 37 | if (call.method.equals("file")) { 38 | final String path = (String) args.get("path"); 39 | String data = buildThumbnailFile(video, path, format, maxhow, quality); 40 | runOnUiThread(()-> result.success(data)); 41 | } else if (call.method.equals("data")) { 42 | byte[] data = buildThumbnailData(video, format, maxhow, quality); 43 | runOnUiThread(()-> result.success(data)); 44 | } else { 45 | runOnUiThread(result::notImplemented); 46 | } 47 | }).start(); 48 | 49 | } catch (Exception e) { 50 | e.printStackTrace(); 51 | result.error("exception", e.getMessage(), null); 52 | } 53 | }); 54 | } 55 | 56 | private static Bitmap.CompressFormat intToFormat(int format) { 57 | switch (format) { 58 | default: 59 | case 0: 60 | return Bitmap.CompressFormat.JPEG; 61 | case 1: 62 | return Bitmap.CompressFormat.PNG; 63 | case 2: 64 | return Bitmap.CompressFormat.WEBP; 65 | } 66 | } 67 | 68 | private static String formatExt(int format) { 69 | switch (format) { 70 | default: 71 | case 0: 72 | return "jpg"; 73 | case 1: 74 | return "png"; 75 | case 2: 76 | return "webp"; 77 | } 78 | } 79 | 80 | private byte[] buildThumbnailData(String vidPath, int format, int maxhow, int quality) { 81 | Log.d(TAG, String.format("buildThumbnailData( format:%d, maxhow:%d, quality:%d )", format, maxhow, quality)); 82 | Bitmap bitmap = createVideoThumbnail(vidPath, maxhow); 83 | if (bitmap == null) 84 | throw new NullPointerException(); 85 | 86 | ByteArrayOutputStream stream = new ByteArrayOutputStream(); 87 | bitmap.compress(intToFormat(format), quality, stream); 88 | bitmap.recycle(); 89 | if (bitmap == null) 90 | throw new NullPointerException(); 91 | return stream.toByteArray(); 92 | } 93 | 94 | private String buildThumbnailFile(String vidPath, String path, int format, int maxhow, int quality) { 95 | Log.d(TAG, String.format("buildThumbnailFile( format:%d, maxhow:%d, quality:%d )", format, maxhow, quality)); 96 | final byte bytes[] = buildThumbnailData(vidPath, format, maxhow, quality); 97 | final String ext = formatExt(format); 98 | final int i = vidPath.lastIndexOf("."); 99 | String fullpath = vidPath.substring(0, i + 1) + ext; 100 | 101 | if (path != null) { 102 | if (path.endsWith(ext)) { 103 | fullpath = path; 104 | } else { 105 | // try to save to same folder as the vidPath 106 | final int j = fullpath.lastIndexOf("/"); 107 | 108 | if (path.endsWith("/")) { 109 | fullpath = path + fullpath.substring(j + 1); 110 | } else { 111 | fullpath = path + fullpath.substring(j); 112 | } 113 | } 114 | } 115 | 116 | try { 117 | FileOutputStream f = new FileOutputStream(fullpath); 118 | f.write(bytes); 119 | f.close(); 120 | Log.d(TAG, String.format("buildThumbnailFile( written:%d )", bytes.length)); 121 | } catch (java.io.IOException e) { 122 | e.getStackTrace(); 123 | throw new RuntimeException(e); 124 | } 125 | return fullpath; 126 | } 127 | 128 | /** 129 | * Create a video thumbnail for a video. May return null if the video is corrupt 130 | * or the format is not supported. 131 | * 132 | * @param video the URI of video 133 | * @param targetSize max width or height of the thumbnail 134 | */ 135 | public static Bitmap createVideoThumbnail(String video, int targetSize) { 136 | Bitmap bitmap = null; 137 | MediaMetadataRetriever retriever = new MediaMetadataRetriever(); 138 | try { 139 | Log.d(TAG, String.format("setDataSource: %s )", video)); 140 | if (video.startsWith("file://") || video.startsWith("/")) { 141 | retriever.setDataSource(video); 142 | } else { 143 | retriever.setDataSource(video, new HashMap()); 144 | } 145 | bitmap = retriever.getFrameAtTime(-1); 146 | } catch (IllegalArgumentException ex) { 147 | ex.printStackTrace(); 148 | } catch (RuntimeException ex) { 149 | ex.printStackTrace(); 150 | } finally { 151 | try { 152 | retriever.release(); 153 | } catch (IOException ex) { 154 | ex.printStackTrace(); 155 | } catch (RuntimeException ex) { 156 | ex.printStackTrace(); 157 | } 158 | } 159 | 160 | if (bitmap == null) 161 | return null; 162 | 163 | if (targetSize != 0) { 164 | int width = bitmap.getWidth(); 165 | int height = bitmap.getHeight(); 166 | int max = Math.max(width, height); 167 | float scale = (float) targetSize / max; 168 | int w = Math.round(scale * width); 169 | int h = Math.round(scale * height); 170 | Log.d(TAG, String.format("original w:%d, h:%d, scale:%6.4f => %d, %d", width, height, scale, w, h)); 171 | bitmap = Bitmap.createScaledBitmap(bitmap, w, h, true); 172 | } 173 | return bitmap; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins; 2 | 3 | import androidx.annotation.Keep; 4 | import androidx.annotation.NonNull; 5 | import io.flutter.Log; 6 | 7 | import io.flutter.embedding.engine.FlutterEngine; 8 | import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry; 9 | 10 | /** 11 | * Generated file. Do not edit. 12 | * This file is generated by the Flutter tool based on the 13 | * plugins that support the Android platform. 14 | */ 15 | @Keep 16 | public final class GeneratedPluginRegistrant { 17 | private static final String TAG = "GeneratedPluginRegistrant"; 18 | public static void registerWith(@NonNull FlutterEngine flutterEngine) { 19 | ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine); 20 | try { 21 | flutterEngine.getPlugins().add(new io.flutter.plugins.firebase.firestore.FlutterFirebaseFirestorePlugin()); 22 | } catch(Exception e) { 23 | Log.e(TAG, "Error registering plugin cloud_firestore, io.flutter.plugins.firebase.firestore.FlutterFirebaseFirestorePlugin", e); 24 | } 25 | try { 26 | it.nplace.downloadspathprovider.DownloadsPathProviderPlugin.registerWith(shimPluginRegistry.registrarFor("it.nplace.downloadspathprovider.DownloadsPathProviderPlugin")); 27 | } catch(Exception e) { 28 | Log.e(TAG, "Error registering plugin downloads_path_provider, it.nplace.downloadspathprovider.DownloadsPathProviderPlugin", e); 29 | } 30 | try { 31 | flutterEngine.getPlugins().add(new com.jeffg.emoji_picker.EmojiPickerPlugin()); 32 | } catch(Exception e) { 33 | Log.e(TAG, "Error registering plugin emoji_picker, com.jeffg.emoji_picker.EmojiPickerPlugin", e); 34 | } 35 | try { 36 | flutterEngine.getPlugins().add(new com.mr.flutter.plugin.filepicker.FilePickerPlugin()); 37 | } catch(Exception e) { 38 | Log.e(TAG, "Error registering plugin file_picker, com.mr.flutter.plugin.filepicker.FilePickerPlugin", e); 39 | } 40 | try { 41 | flutterEngine.getPlugins().add(new io.flutter.plugins.firebase.auth.FlutterFirebaseAuthPlugin()); 42 | } catch(Exception e) { 43 | Log.e(TAG, "Error registering plugin firebase_auth, io.flutter.plugins.firebase.auth.FlutterFirebaseAuthPlugin", e); 44 | } 45 | try { 46 | flutterEngine.getPlugins().add(new io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin()); 47 | } catch(Exception e) { 48 | Log.e(TAG, "Error registering plugin firebase_core, io.flutter.plugins.firebase.core.FlutterFirebaseCorePlugin", e); 49 | } 50 | try { 51 | flutterEngine.getPlugins().add(new io.flutter.plugins.firebase.storage.FlutterFirebaseStoragePlugin()); 52 | } catch(Exception e) { 53 | Log.e(TAG, "Error registering plugin firebase_storage, io.flutter.plugins.firebase.storage.FlutterFirebaseStoragePlugin", e); 54 | } 55 | try { 56 | flutterEngine.getPlugins().add(new vn.hunghd.flutterdownloader.FlutterDownloaderPlugin()); 57 | } catch(Exception e) { 58 | Log.e(TAG, "Error registering plugin flutter_downloader, vn.hunghd.flutterdownloader.FlutterDownloaderPlugin", e); 59 | } 60 | try { 61 | flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin()); 62 | } catch(Exception e) { 63 | Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e); 64 | } 65 | try { 66 | flutterEngine.getPlugins().add(new io.flutter.plugins.googlesignin.GoogleSignInPlugin()); 67 | } catch(Exception e) { 68 | Log.e(TAG, "Error registering plugin google_sign_in_android, io.flutter.plugins.googlesignin.GoogleSignInPlugin", e); 69 | } 70 | try { 71 | flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin()); 72 | } catch(Exception e) { 73 | Log.e(TAG, "Error registering plugin image_picker, io.flutter.plugins.imagepicker.ImagePickerPlugin", e); 74 | } 75 | try { 76 | flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); 77 | } catch(Exception e) { 78 | Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e); 79 | } 80 | try { 81 | flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin()); 82 | } catch(Exception e) { 83 | Log.e(TAG, "Error registering plugin shared_preferences, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e); 84 | } 85 | try { 86 | flutterEngine.getPlugins().add(new com.tekartik.sqflite.SqflitePlugin()); 87 | } catch(Exception e) { 88 | Log.e(TAG, "Error registering plugin sqflite, com.tekartik.sqflite.SqflitePlugin", e); 89 | } 90 | try { 91 | flutterEngine.getPlugins().add(new io.flutter.plugins.videoplayer.VideoPlayerPlugin()); 92 | } catch(Exception e) { 93 | Log.e(TAG, "Error registering plugin video_player_android, io.flutter.plugins.videoplayer.VideoPlayerPlugin", e); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:4.2.0' 9 | classpath 'com.google.gms:google-services:4.3.3' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | jcenter() 17 | } 18 | } 19 | 20 | rootProject.buildDir = '../build' 21 | subprojects { 22 | project.buildDir = "${rootProject.buildDir}/${project.name}" 23 | } 24 | subprojects { 25 | project.evaluationDependsOn(':app') 26 | } 27 | subprojects { 28 | 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jun 16 15:59:50 ICT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/local.properties: -------------------------------------------------------------------------------- 1 | sdk.dir=/Users/adityadroid/Library/Android/sdk 2 | flutter.sdk=/Users/adityadroid/Development/flutter 3 | flutter.buildMode=debug 4 | flutter.versionName=1.0.0 5 | flutter.versionCode=1 -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /assets/fonts/manrope-bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/fonts/manrope-bold.otf -------------------------------------------------------------------------------- /assets/fonts/manrope-extrabold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/fonts/manrope-extrabold.otf -------------------------------------------------------------------------------- /assets/fonts/manrope-light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/fonts/manrope-light.otf -------------------------------------------------------------------------------- /assets/fonts/manrope-medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/fonts/manrope-medium.otf -------------------------------------------------------------------------------- /assets/fonts/manrope-regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/fonts/manrope-regular.otf -------------------------------------------------------------------------------- /assets/fonts/manrope-semibold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/fonts/manrope-semibold.otf -------------------------------------------------------------------------------- /assets/fonts/manrope-thin.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/fonts/manrope-thin.otf -------------------------------------------------------------------------------- /assets/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/google.png -------------------------------------------------------------------------------- /assets/launcher/ic_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/launcher/ic_background.png -------------------------------------------------------------------------------- /assets/launcher/ic_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/launcher/ic_foreground.png -------------------------------------------------------------------------------- /assets/launcher/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/launcher/ic_launcher.png -------------------------------------------------------------------------------- /assets/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/placeholder.png -------------------------------------------------------------------------------- /assets/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/social.png -------------------------------------------------------------------------------- /assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityadroid/Messio/d65473b7f91d1cf7ac09c6b4c9fe35ad08febd29/assets/user.png -------------------------------------------------------------------------------- /howModalBottomSheet: -------------------------------------------------------------------------------- 1 | ChatBloc 2 | ChatList 3 | ContactBloc 4 | ContactList 5 | Conversation-Attachment 6 | ConversationUI 7 | ConversationUI-Patch 8 | ConversationUI-Test 9 | * DarkTheme 10 | HomePage 11 | ReadmePatch 12 | RegisterUI 13 | Registration-Bloc 14 | Registration-Test 15 | Travis-CI 16 | init 17 | master 18 | -------------------------------------------------------------------------------- /lib/blocs/attachments/attachments_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'package:bloc/bloc.dart'; 4 | import 'package:file_picker/file_picker.dart'; 5 | import 'package:messio/models/message.dart'; 6 | import 'package:messio/models/video_wrapper.dart'; 7 | import 'package:messio/repositories/chat_repository.dart'; 8 | import 'package:messio/utils/shared_objects.dart'; 9 | import './bloc.dart'; 10 | 11 | class AttachmentsBloc extends Bloc { 12 | final ChatRepository chatRepository; 13 | 14 | AttachmentsBloc({this.chatRepository}) :super(InitialAttachmentsState()){ 15 | assert(chatRepository != null); 16 | on(mapFetchAttachmentsEventToState); 17 | } 18 | 19 | Future mapFetchAttachmentsEventToState( 20 | FetchAttachmentsEvent event, Emitter emit) async { 21 | int type = SharedObjects.getTypeFromFileType(event.fileType); 22 | List attachments = 23 | await chatRepository.getAttachments(event.chatId, type); 24 | if (event.fileType != FileType.video) { 25 | emit(FetchedAttachmentsState(event.fileType, attachments)); 26 | } else { 27 | List videos = List(); 28 | for(Message message in attachments){ 29 | if (message is VideoMessage) { 30 | File file = await SharedObjects.getThumbnail(message.videoUrl); 31 | videos.add(VideoWrapper(file, message)); 32 | } 33 | } 34 | emit(FetchedVideosState(videos)); 35 | } 36 | } 37 | 38 | FutureOr> parseVideos(List attachments) async{ 39 | List videos = List(); 40 | for(Message message in attachments){ 41 | if (message is VideoMessage) { 42 | File file = await SharedObjects.getThumbnail(message.videoUrl); 43 | videos.add(VideoWrapper(file, message)); 44 | } 45 | } 46 | return videos; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/blocs/attachments/attachments_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:file_picker/file_picker.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | @immutable 6 | abstract class AttachmentsEvent extends Equatable { 7 | AttachmentsEvent([List props = const []]); 8 | 9 | @override 10 | List get props => []; 11 | } 12 | class FetchAttachmentsEvent extends AttachmentsEvent{ 13 | final FileType fileType; 14 | final String chatId; 15 | FetchAttachmentsEvent(this.chatId,this.fileType): super([chatId,fileType]); 16 | 17 | @override 18 | String toString() => 'FetchAttachmentsEvent { chatId : $chatId fileType : $fileType }'; 19 | } 20 | -------------------------------------------------------------------------------- /lib/blocs/attachments/attachments_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:file_picker/file_picker.dart'; 3 | import 'package:messio/models/message.dart'; 4 | import 'package:messio/models/video_wrapper.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | @immutable 8 | abstract class AttachmentsState extends Equatable { 9 | AttachmentsState([List props = const []]); 10 | @override 11 | List get props => []; 12 | } 13 | 14 | class InitialAttachmentsState extends AttachmentsState {} 15 | 16 | class FetchedAttachmentsState extends AttachmentsState{ 17 | final List attachments; 18 | final FileType fileType; 19 | 20 | FetchedAttachmentsState(this.fileType,this.attachments): super([attachments, fileType]); 21 | 22 | @override 23 | String toString() => 'FetchedAttachmentsState { attachments : $attachments , fileType : $fileType}'; 24 | } 25 | class FetchedVideosState extends AttachmentsState{ 26 | final List videos; 27 | 28 | FetchedVideosState(this.videos): super([videos]); 29 | 30 | @override 31 | String toString() => 'FetchedVideosState { videos : $videos }'; 32 | } -------------------------------------------------------------------------------- /lib/blocs/attachments/bloc.dart: -------------------------------------------------------------------------------- 1 | export 'attachments_bloc.dart'; 2 | export 'attachments_event.dart'; 3 | export 'attachments_state.dart'; -------------------------------------------------------------------------------- /lib/blocs/authentication/authentication_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'package:bloc/bloc.dart'; 4 | import 'package:firebase_auth/firebase_auth.dart'; 5 | import 'package:messio/config/paths.dart'; 6 | import 'package:messio/repositories/authentication_repository.dart'; 7 | import 'package:messio/repositories/storage_repository.dart'; 8 | import 'package:messio/repositories/user_data_repository.dart'; 9 | import './bloc.dart'; 10 | 11 | class AuthenticationBloc 12 | extends Bloc { 13 | final AuthenticationRepository authenticationRepository; 14 | final UserDataRepository userDataRepository; 15 | final StorageRepository storageRepository; 16 | 17 | AuthenticationBloc( 18 | {this.authenticationRepository, 19 | this.userDataRepository, 20 | this.storageRepository}) : super(Uninitialized()){ 21 | assert(authenticationRepository != null); 22 | assert(userDataRepository != null); 23 | assert(storageRepository != null); 24 | on(mapAppLaunchedToState); 25 | on(mapClickedGoogleLoginToState); 26 | on(mapLoggedInToState); 27 | on(mapPickedProfilePictureToState); 28 | on(mapSaveProfileToState); 29 | on(mapLoggedOutToState); 30 | } 31 | 32 | Future mapPickedProfilePictureToState(PickedProfilePicture event,Emitter emit) async { 33 | emit(ReceivedProfilePicture(event.file)); 34 | } 35 | 36 | Future mapAppLaunchedToState(AppLaunched event, Emitter emit) async { 37 | try { 38 | emit(AuthInProgress()); //show the progress bar 39 | final isSignedIn = authenticationRepository.isLoggedIn(); // check if user is signed in 40 | if (isSignedIn) { 41 | final user = authenticationRepository.getCurrentUser(); 42 | bool isProfileComplete = 43 | await userDataRepository.isProfileComplete(); // if he is signed in then check if his profile is complete 44 | print(isProfileComplete); 45 | if (isProfileComplete) { //if profile is complete then redirect to the home page 46 | emit(ProfileUpdated()); 47 | } else { 48 | emit(Authenticated(user)); // else yield the authenticated state and redirect to profile page to complete profile. 49 | add(LoggedIn(user)); // also disptach a login event so that the data from gauth can be prefilled 50 | } 51 | } else { 52 | emit(UnAuthenticated()); // is not signed in then show the home page 53 | } 54 | } catch (_, stacktrace) { 55 | print(stacktrace); 56 | emit(UnAuthenticated()); 57 | } 58 | } 59 | 60 | Future mapClickedGoogleLoginToState(ClickedGoogleLogin event, Emitter emit) async { 61 | emit(AuthInProgress()); //show progress bar 62 | try { 63 | User firebaseUser = 64 | await authenticationRepository.signInWithGoogle(); // show the google auth prompt and wait for user selection, retrieve the selected account 65 | bool isProfileComplete = 66 | await userDataRepository.isProfileComplete(); // check if the user's profile is complete 67 | print('isProfileComplete $isProfileComplete'); 68 | if (isProfileComplete) { 69 | emit(ProfileUpdated()); //if profile is complete go to home page 70 | } else { 71 | emit(Authenticated(firebaseUser)); // else yield the authenticated state and redirect to profile page to complete profile. 72 | add(LoggedIn(firebaseUser)); // also dispatch a login event so that the data from gauth can be prefilled 73 | } 74 | } catch (_, stacktrace) { 75 | print(stacktrace); 76 | emit(UnAuthenticated()); // in case of error go back to first registration page 77 | } 78 | } 79 | 80 | Future mapLoggedInToState( 81 | LoggedIn event,Emitter emit) async { 82 | emit(ProfileUpdateInProgress()); // shows progress bar 83 | final user = await userDataRepository.saveDetailsFromGoogleAuth(event.user); // save the gAuth details to firestore database 84 | emit(PreFillData(user)); // prefill the gauth data in the form 85 | } 86 | 87 | Future mapSaveProfileToState( 88 | SaveProfile event, Emitter emit) async { 89 | emit(ProfileUpdateInProgress()); // shows progress bar 90 | String profilePictureUrl = await storageRepository.uploadFile( 91 | event.profileImage, Paths.profilePicturePath); // upload image to firebase storage 92 | final user = authenticationRepository.getCurrentUser(); // retrieve user from firebase 93 | await userDataRepository.saveProfileDetails( 94 | user.uid, profilePictureUrl, event.age, event.username); // save profile details to firestore 95 | emit(ProfileUpdated()); //redirect to home page 96 | } 97 | 98 | Future mapLoggedOutToState(ClickedLogout event, Emitter emit) async { 99 | emit(UnAuthenticated()); // redirect to login page 100 | authenticationRepository.signOutUser(); // terminate session 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /lib/blocs/authentication/authentication_event.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:firebase_auth/firebase_auth.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | @immutable 7 | abstract class AuthenticationEvent extends Equatable { 8 | AuthenticationEvent([List props = const []]); 9 | @override 10 | List get props => []; 11 | } 12 | 13 | class AppLaunched extends AuthenticationEvent { 14 | @override 15 | String toString() => 'AppLaunched'; 16 | } 17 | 18 | class ClickedGoogleLogin extends AuthenticationEvent { 19 | @override 20 | String toString() => 'ClickedGoogleLogin'; 21 | } 22 | 23 | class LoggedIn extends AuthenticationEvent { 24 | final User user; 25 | LoggedIn(this.user): super([user]); 26 | @override 27 | String toString() => 'LoggedIn'; 28 | } 29 | 30 | class PickedProfilePicture extends AuthenticationEvent{ 31 | final File file; 32 | PickedProfilePicture(this.file): super([file]); 33 | @override 34 | String toString() => 'PickedProfilePicture'; 35 | } 36 | 37 | class SaveProfile extends AuthenticationEvent { 38 | final File profileImage; 39 | final int age; 40 | final String username; 41 | SaveProfile(this.profileImage, this.age, this.username): super([profileImage,age,username]); 42 | @override 43 | String toString() => 'SaveProfile'; 44 | } 45 | 46 | class ClickedLogout extends AuthenticationEvent { 47 | @override 48 | String toString() => 'ClickedLogout'; 49 | } 50 | -------------------------------------------------------------------------------- /lib/blocs/authentication/authentication_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:firebase_auth/firebase_auth.dart'; 4 | import 'package:messio/models/messio_user.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | @immutable 8 | abstract class AuthenticationState extends Equatable { 9 | AuthenticationState([List props = const []]); 10 | @override 11 | List get props => []; 12 | } 13 | 14 | class Uninitialized extends AuthenticationState{ 15 | @override 16 | String toString() => 'Uninitialized'; 17 | } 18 | 19 | class AuthInProgress extends AuthenticationState{ 20 | @override 21 | String toString() => 'AuthInProgress'; 22 | } 23 | 24 | class Authenticated extends AuthenticationState{ 25 | final User user; 26 | Authenticated(this.user); 27 | @override 28 | String toString() => 'Authenticated'; 29 | } 30 | 31 | class PreFillData extends AuthenticationState{ 32 | final MessioUser user; 33 | PreFillData(this.user); 34 | @override 35 | String toString() => 'PreFillData'; 36 | } 37 | 38 | class UnAuthenticated extends AuthenticationState{ 39 | @override 40 | String toString() => 'UnAuthenticated'; 41 | } 42 | 43 | class ReceivedProfilePicture extends AuthenticationState{ 44 | final File file; 45 | ReceivedProfilePicture(this.file); 46 | @override toString() => 'ReceivedProfilePicture'; 47 | } 48 | 49 | class ProfileUpdateInProgress extends AuthenticationState{ 50 | @override 51 | String toString() => 'ProfileUpdateInProgress'; 52 | } 53 | 54 | class ProfileUpdated extends AuthenticationState{ 55 | @override 56 | String toString() => 'ProfileComplete'; 57 | } 58 | -------------------------------------------------------------------------------- /lib/blocs/authentication/bloc.dart: -------------------------------------------------------------------------------- 1 | export 'authentication_bloc.dart'; 2 | export 'authentication_event.dart'; 3 | export 'authentication_state.dart'; -------------------------------------------------------------------------------- /lib/blocs/chats/bloc.dart: -------------------------------------------------------------------------------- 1 | export 'chat_bloc.dart'; 2 | export 'chat_event.dart'; 3 | export 'chat_state.dart'; -------------------------------------------------------------------------------- /lib/blocs/chats/chat_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'package:bloc/bloc.dart'; 4 | import 'package:file_picker/file_picker.dart'; 5 | import 'package:path/path.dart'; 6 | import 'package:messio/blocs/chats/bloc.dart'; 7 | import 'package:messio/config/constants.dart'; 8 | import 'package:messio/config/paths.dart'; 9 | import 'package:messio/models/message.dart'; 10 | import 'package:messio/models/messio_user.dart'; 11 | import 'package:messio/repositories/chat_repository.dart'; 12 | import 'package:messio/repositories/storage_repository.dart'; 13 | import 'package:messio/repositories/user_data_repository.dart'; 14 | import 'package:messio/utils/exceptions.dart'; 15 | import 'package:messio/utils/shared_objects.dart'; 16 | 17 | class ChatBloc extends Bloc { 18 | final ChatRepository chatRepository; 19 | final UserDataRepository userDataRepository; 20 | final StorageRepository storageRepository; 21 | Map messagesSubscriptionMap = Map(); 22 | StreamSubscription chatsSubscription; 23 | String activeChatId; 24 | 25 | ChatBloc( 26 | {this.chatRepository, this.userDataRepository, this.storageRepository}) 27 | : super(InitialChatState()){assert(chatRepository != null); 28 | assert(userDataRepository != null); 29 | assert(storageRepository != null); 30 | on(mapFetchChatListEventToState); 31 | on(mapRegisterActiveChatEventToState); 32 | on(mapReceivedChatsEventToState); 33 | on(mapPageChangedEventToState); 34 | on(mapFetchConversationDetailsEventToState); 35 | on(mapFetchMessagesEventToState); 36 | on(mapFetchPreviousMessagesEventToState); 37 | on(mapReceivedMessagesEventToState); 38 | on(mapSendTextMessageEventToState); 39 | on(mapSendAttachmentEventToState); 40 | on(mapToggleEmojiKeyboardEventToState); 41 | } 42 | 43 | Future mapFetchChatListEventToState( 44 | FetchChatListEvent event, Emitter emit) async { 45 | try { 46 | chatsSubscription?.cancel(); 47 | chatsSubscription = chatRepository 48 | .getChats() 49 | .listen((chats) => add(ReceivedChatsEvent(chats))); 50 | } on MessioException catch (exception) { 51 | print(exception.errorMessage()); 52 | emit(ErrorState(exception)); 53 | } 54 | } 55 | 56 | Future mapFetchMessagesEventToState( 57 | FetchMessagesEvent event, Emitter emit) async { 58 | try { 59 | emit(FetchingMessageState()); 60 | String chatId = 61 | await chatRepository.getChatIdByUsername(event.chat.username); 62 | // print('mapFetchMessagesEventToState'); 63 | // print('MessSubMap: $messagesSubscriptionMap'); 64 | StreamSubscription messagesSubscription = messagesSubscriptionMap[chatId]; 65 | messagesSubscription?.cancel(); 66 | messagesSubscription = chatRepository.getMessages(chatId).listen((messages) => add(ReceivedMessagesEvent(messages, event.chat.username))); 67 | 68 | messagesSubscriptionMap[chatId] = messagesSubscription; 69 | } on MessioException catch (exception) { 70 | print(exception.errorMessage()); 71 | emit(ErrorState(exception)); 72 | } 73 | } 74 | 75 | Future mapFetchPreviousMessagesEventToState( 76 | FetchPreviousMessagesEvent event, Emitter emit) async { 77 | try { 78 | String chatId = 79 | await chatRepository.getChatIdByUsername(event.chat.username); 80 | final messages = 81 | await chatRepository.getPreviousMessages(chatId, event.lastMessage); 82 | emit(FetchedMessagesState(messages, event.chat.username, 83 | isPrevious: true)); 84 | } on MessioException catch (exception) { 85 | print(exception.errorMessage()); 86 | emit(ErrorState(exception)); 87 | } 88 | } 89 | 90 | Future mapFetchConversationDetailsEventToState( 91 | FetchConversationDetailsEvent event, Emitter emit) async { 92 | print('fetching details fro ${event.chat.username}'); 93 | MessioUser user = await userDataRepository.getUser(event.chat.username); 94 | emit(FetchedContactDetailsState(user, event.chat.username)); 95 | add(FetchMessagesEvent(event.chat)); 96 | } 97 | 98 | Future mapSendAttachmentEventToState(SendAttachmentEvent event, Emitter emit) async { 99 | File file = event.file; 100 | String fileName = basename(file.path); 101 | String url = await storageRepository.uploadFile( 102 | file, Paths.getAttachmentPathByFileType(event.fileType)); 103 | String username = SharedObjects.prefs.getString(Constants.sessionUsername); 104 | String name = SharedObjects.prefs.getString(Constants.sessionName); 105 | print('File Name: $fileName'); 106 | Message message; 107 | if (event.fileType == FileType.image) 108 | message = ImageMessage( 109 | url, fileName, DateTime.now().millisecondsSinceEpoch, name, username); 110 | else if (event.fileType == FileType.video) 111 | message = VideoMessage( 112 | url, fileName, DateTime.now().millisecondsSinceEpoch, name, username); 113 | else 114 | message = FileMessage( 115 | url, fileName, DateTime.now().millisecondsSinceEpoch, name, username); 116 | await chatRepository.sendMessage(event.chatId, message); 117 | } 118 | 119 | Future mapRegisterActiveChatEventToState(RegisterActiveChatEvent event, Emitter emit) async { 120 | activeChatId = event.activeChatId; 121 | } 122 | 123 | Future mapReceivedChatsEventToState(ReceivedChatsEvent event, Emitter emit)async { 124 | emit(FetchedChatListState(event.chatList)); 125 | } 126 | 127 | Future mapPageChangedEventToState(PageChangedEvent event, Emitter emit)async { 128 | activeChatId = event.activeChat.chatId; 129 | emit(PageChangedState(event.index, event.activeChat)); 130 | } 131 | 132 | Future mapReceivedMessagesEventToState(ReceivedMessagesEvent event, Emitter emit) async { 133 | print('dispatching received messages'); 134 | emit(FetchedMessagesState(event.messages, event.username, 135 | isPrevious: false)); 136 | } 137 | 138 | Future mapSendTextMessageEventToState(SendTextMessageEvent event, Emitter emit) async{ 139 | Message message = TextMessage( 140 | event.message, 141 | DateTime.now().millisecondsSinceEpoch, 142 | SharedObjects.prefs.getString(Constants.sessionName), 143 | SharedObjects.prefs.getString(Constants.sessionUsername)); 144 | await chatRepository.sendMessage(activeChatId, message); 145 | } 146 | 147 | Future mapToggleEmojiKeyboardEventToState(ToggleEmojiKeyboardEvent event, Emitter emit)async { 148 | emit(ToggleEmojiKeyboardState(event.showEmojiKeyboard)); 149 | 150 | } 151 | 152 | @override 153 | Future close() { 154 | messagesSubscriptionMap.forEach((_, subscription) => subscription.cancel()); 155 | return super.close(); 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /lib/blocs/chats/chat_event.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:equatable/equatable.dart'; 4 | import 'package:file_picker/file_picker.dart'; 5 | import 'package:messio/models/chat.dart'; 6 | import 'package:messio/models/message.dart'; 7 | import 'package:meta/meta.dart'; 8 | 9 | @immutable 10 | abstract class ChatEvent extends Equatable { 11 | ChatEvent([List props = const []]); 12 | @override 13 | List get props => []; 14 | } 15 | 16 | 17 | //triggered to fetch list of chats 18 | class FetchChatListEvent extends ChatEvent { 19 | @override 20 | String toString() => 'FetchChatListEvent'; 21 | } 22 | 23 | 24 | //triggered when stream containing list of chats has new data 25 | class ReceivedChatsEvent extends ChatEvent { 26 | final List chatList; 27 | 28 | ReceivedChatsEvent(this.chatList); 29 | 30 | @override 31 | String toString() => 'ReceivedChatsEvent'; 32 | } 33 | 34 | //triggered to get details of currently open conversation 35 | class FetchConversationDetailsEvent extends ChatEvent { 36 | final Chat chat; 37 | 38 | FetchConversationDetailsEvent(this.chat) : super([chat]); 39 | 40 | @override 41 | String toString() => 'FetchConversationDetailsEvent'; 42 | } 43 | 44 | //triggered to fetch messages of chat, this will also keep a subscription for new messages 45 | class FetchMessagesEvent extends ChatEvent { 46 | final Chat chat; 47 | FetchMessagesEvent(this.chat,) : super([chat]); 48 | 49 | @override 50 | String toString() => 'FetchMessagesEvent'; 51 | } 52 | //triggered to fetch messages of chat 53 | class FetchPreviousMessagesEvent extends ChatEvent { 54 | final Chat chat; 55 | final Message lastMessage; 56 | FetchPreviousMessagesEvent(this.chat,this.lastMessage) : super([chat,lastMessage]); 57 | 58 | @override 59 | String toString() => 'FetchPreviousMessagesEvent'; 60 | } 61 | 62 | //triggered when messages stream has new data 63 | class ReceivedMessagesEvent extends ChatEvent { 64 | final List messages; 65 | final String username; 66 | ReceivedMessagesEvent(this.messages, this.username) : super([messages, username]); 67 | 68 | @override 69 | String toString() => 'ReceivedMessagesEvent'; 70 | } 71 | 72 | //triggered to send new text message 73 | class SendTextMessageEvent extends ChatEvent { 74 | final String message; 75 | 76 | SendTextMessageEvent(this.message) : super([message]); 77 | 78 | @override 79 | String toString() => 'SendTextMessageEvent {message: $message}'; 80 | } 81 | 82 | //triggered to send attachment 83 | class SendAttachmentEvent extends ChatEvent { 84 | final String chatId; 85 | final File file; 86 | final FileType fileType; 87 | 88 | SendAttachmentEvent(this.chatId, this.file, this.fileType) 89 | : super([chatId, file, fileType]); 90 | 91 | @override 92 | String toString() => 'SendAttachmentEvent'; 93 | } 94 | 95 | //triggered on page change 96 | class PageChangedEvent extends ChatEvent { 97 | final int index; 98 | final Chat activeChat; 99 | PageChangedEvent(this.index, this.activeChat) : super([index, activeChat]); 100 | 101 | @override 102 | String toString() => 'PageChangedEvent {index: $index, activeChat: $activeChat}'; 103 | } 104 | 105 | class RegisterActiveChatEvent extends ChatEvent{ 106 | final String activeChatId; 107 | RegisterActiveChatEvent(this.activeChatId); 108 | @override 109 | String toString() => 'RegisterActiveChatEvent { activeChatId : $activeChatId }'; 110 | } 111 | 112 | // hide/show emojikeyboard 113 | class ToggleEmojiKeyboardEvent extends ChatEvent{ 114 | final bool showEmojiKeyboard; 115 | 116 | ToggleEmojiKeyboardEvent(this.showEmojiKeyboard); 117 | 118 | @override 119 | String toString() => 'ToggleEmojiKeyboardEvent'; 120 | } -------------------------------------------------------------------------------- /lib/blocs/chats/chat_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:messio/models/chat.dart'; 3 | import 'package:messio/models/message.dart'; 4 | import 'package:messio/models/messio_user.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | @immutable 8 | abstract class ChatState extends Equatable { 9 | ChatState([List props = const []]); 10 | 11 | @override 12 | List get props => []; 13 | } 14 | 15 | class InitialChatState extends ChatState {} 16 | 17 | class FetchedChatListState extends ChatState { 18 | final List chatList; 19 | 20 | FetchedChatListState(this.chatList) : super([chatList]); 21 | 22 | @override 23 | String toString() => 'FetchedChatListState'; 24 | } 25 | class FetchingMessageState extends ChatState{ 26 | @override 27 | String toString() => 'FetchingMessageState'; 28 | } 29 | 30 | 31 | class FetchedMessagesState extends ChatState { 32 | final List messages; 33 | final String username; 34 | final isPrevious; 35 | FetchedMessagesState(this.messages,this.username, {this.isPrevious}) : super([messages,username,isPrevious]); 36 | 37 | @override 38 | String toString() => 'FetchedMessagesState {messages: ${messages.length}, username: $username, isPrevious: $isPrevious}'; 39 | } 40 | 41 | class ErrorState extends ChatState { 42 | final Exception exception; 43 | 44 | ErrorState(this.exception) : super([exception]); 45 | 46 | @override 47 | String toString() => 'ErrorState'; 48 | } 49 | 50 | class FetchedContactDetailsState extends ChatState { 51 | final MessioUser user; 52 | final String username; 53 | FetchedContactDetailsState(this.user,this.username) : super([user,username]); 54 | 55 | @override 56 | String toString() => 'FetchedContactDetailsState'; 57 | } 58 | 59 | class PageChangedState extends ChatState { 60 | final int index; 61 | final Chat activeChat; 62 | PageChangedState(this.index, this.activeChat) : super([index, activeChat]); 63 | 64 | @override 65 | String toString() => 'PageChangedState'; 66 | } 67 | 68 | class ToggleEmojiKeyboardState extends ChatState{ 69 | final bool showEmojiKeyboard; 70 | 71 | ToggleEmojiKeyboardState(this.showEmojiKeyboard): super([showEmojiKeyboard]); 72 | 73 | @override 74 | String toString() => 'ToggleEmojiKeyboardState'; 75 | } 76 | -------------------------------------------------------------------------------- /lib/blocs/config/bloc.dart: -------------------------------------------------------------------------------- 1 | export 'config_bloc.dart'; 2 | export 'config_event.dart'; 3 | export 'config_state.dart'; -------------------------------------------------------------------------------- /lib/blocs/config/config_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:messio/config/paths.dart'; 4 | import 'package:messio/repositories/storage_repository.dart'; 5 | import 'package:messio/repositories/user_data_repository.dart'; 6 | import 'package:messio/utils/shared_objects.dart'; 7 | import 'bloc.dart'; 8 | 9 | class ConfigBloc extends Bloc { 10 | UserDataRepository userDataRepository; 11 | StorageRepository storageRepository; 12 | 13 | ConfigBloc({this.userDataRepository, this.storageRepository}) 14 | : super(UnConfigState()){assert(userDataRepository != null); 15 | assert(storageRepository != null); 16 | on(mapConfigValueChangedToState); 17 | on(mapUpdateProfilePictureToState); 18 | on(mapRestartAppToState); 19 | } 20 | 21 | Future mapUpdateProfilePictureToState( 22 | UpdateProfilePicture event,Emitter emit) async { 23 | emit(UpdatingProfilePictureState()); 24 | final profilePictureUrl = await storageRepository.uploadFile(event.file, Paths.profilePicturePath); 25 | await userDataRepository.updateProfilePicture(profilePictureUrl); 26 | emit(ProfilePictureChangedState(profilePictureUrl)); 27 | } 28 | 29 | Future mapConfigValueChangedToState(ConfigValueChanged event, Emitter emit) async { 30 | SharedObjects.prefs.setBool(event.key, event.value); 31 | emit(ConfigChangeState(event.key, event.value)); 32 | } 33 | 34 | Future mapRestartAppToState(RestartApp event, Emitter emit)async { 35 | emit(RestartedAppState()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/blocs/config/config_event.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:equatable/equatable.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | @immutable 7 | abstract class ConfigEvent extends Equatable { 8 | ConfigEvent([List props = const []]); 9 | @override 10 | List get props => []; 11 | } 12 | 13 | class ConfigValueChanged extends ConfigEvent{ 14 | final String key; 15 | final bool value; 16 | ConfigValueChanged(this.key,this.value): super([key,value]); 17 | } 18 | 19 | class UpdateProfilePicture extends ConfigEvent{ 20 | final File file; 21 | UpdateProfilePicture(this.file): super([file]); 22 | @override 23 | String toString() => 'UpdateProfilePicture'; 24 | } 25 | class RestartApp extends ConfigEvent{ 26 | @override 27 | String toString() => 'RestartApp'; 28 | } -------------------------------------------------------------------------------- /lib/blocs/config/config_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | @immutable 6 | abstract class ConfigState extends Equatable { 7 | ConfigState([List props = const []]); 8 | @override 9 | List get props => []; 10 | } 11 | 12 | class ConfigChangeState extends ConfigState{ 13 | final String key; 14 | final bool value; 15 | ConfigChangeState(this.key,this.value): super([key,value]); 16 | } 17 | class UnConfigState extends ConfigState{} 18 | class UpdatingProfilePictureState extends ConfigState{} 19 | class ProfilePictureChangedState extends ConfigState{ 20 | final String profilePictureUrl; 21 | ProfilePictureChangedState(this.profilePictureUrl):super([profilePictureUrl]); 22 | @override 23 | String toString()=> 'ProfilePictureChangedState {profilePictureUrl: $profilePictureUrl}'; 24 | } 25 | class RestartedAppState extends ConfigState{ 26 | RestartedAppState():super([]); 27 | } -------------------------------------------------------------------------------- /lib/blocs/contacts/bloc.dart: -------------------------------------------------------------------------------- 1 | export 'contacts_bloc.dart'; 2 | export 'contacts_event.dart'; 3 | export 'contacts_state.dart'; -------------------------------------------------------------------------------- /lib/blocs/contacts/contacts_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:messio/models/messio_user.dart'; 4 | import 'package:messio/repositories/chat_repository.dart'; 5 | import 'package:messio/repositories/user_data_repository.dart'; 6 | import 'package:messio/utils/exceptions.dart'; 7 | import './bloc.dart'; 8 | 9 | class ContactsBloc extends Bloc { 10 | UserDataRepository userDataRepository; 11 | ChatRepository chatRepository; 12 | StreamSubscription subscription; 13 | 14 | ContactsBloc({this.userDataRepository, this.chatRepository}) 15 | : super(InitialContactsState()){ 16 | assert(userDataRepository != null); 17 | assert(chatRepository != null); 18 | on(mapFetchContactsEventToState); 19 | on(mapReceivedContactsEventToState); 20 | on(mapAddContactEventToState); 21 | } 22 | Future mapReceivedContactsEventToState(ReceivedContactsEvent event, Emitter emit)async{ 23 | emit(FetchedContactsState(event.contacts)); 24 | } 25 | Future mapFetchContactsEventToState(FetchContactsEvent event, Emitter emit) async { 26 | try { 27 | emit(FetchingContactsState()); 28 | subscription?.cancel(); 29 | subscription = userDataRepository.getContacts().listen((contacts) => { 30 | print('dispatching $contacts'), 31 | add(ReceivedContactsEvent(contacts)) 32 | }); 33 | } on MessioException catch (exception) { 34 | print(exception.errorMessage()); 35 | emit(ErrorState(exception)); 36 | } 37 | } 38 | 39 | Future mapAddContactEventToState(AddContactEvent event,Emitter emit) async { 40 | userDataRepository.getUser(event.username); 41 | try { 42 | emit(AddContactProgressState()); 43 | await userDataRepository.addContact(event.username); 44 | MessioUser user = await userDataRepository.getUser(event.username); 45 | await chatRepository.createChatIdForContact(user); 46 | emit(AddContactSuccessState()); 47 | } on MessioException catch (exception) { 48 | print(exception.errorMessage()); 49 | emit( AddContactFailedState(exception)); 50 | } 51 | } 52 | 53 | @override 54 | Future close() { 55 | subscription.cancel(); 56 | return super.close(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/blocs/contacts/contacts_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:messio/models/contact.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | @immutable 6 | abstract class ContactsEvent extends Equatable { 7 | ContactsEvent([List props = const []]); 8 | @override 9 | List get props => []; 10 | } 11 | 12 | // Fetch the contacts from firebase 13 | class FetchContactsEvent extends ContactsEvent{ 14 | @override 15 | String toString() => 'FetchContactsEvent'; 16 | } 17 | 18 | // Dispatch received contacts from stream 19 | class ReceivedContactsEvent extends ContactsEvent{ 20 | final List contacts; 21 | ReceivedContactsEvent(this.contacts) : super([contacts]); 22 | @override 23 | String toString() => 'ReceivedContactsEvent'; 24 | } 25 | 26 | //Add a new contact 27 | class AddContactEvent extends ContactsEvent { 28 | final String username; 29 | AddContactEvent({@required this.username}): super([username]); 30 | @override 31 | String toString() => 'AddContactEvent'; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /lib/blocs/contacts/contacts_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:messio/models/contact.dart'; 3 | import 'package:messio/utils/exceptions.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | @immutable 7 | abstract class ContactsState extends Equatable { 8 | ContactsState([List props = const []]); 9 | @override 10 | List get props => []; 11 | } 12 | 13 | class InitialContactsState extends ContactsState { 14 | @override 15 | String toString() => 'InitialContactsState'; 16 | } 17 | //Fetching contacts from firebase 18 | class FetchingContactsState extends ContactsState{ 19 | @override 20 | String toString() => 'FetchingContactsState'; 21 | } 22 | //contacts fetched successfully 23 | class FetchedContactsState extends ContactsState { 24 | final List contacts; 25 | FetchedContactsState(this.contacts): super([contacts]); 26 | @override 27 | String toString() => 'FetchedContactsState'; 28 | } 29 | 30 | // Add Contact Clicked, show progressbar 31 | class AddContactProgressState extends ContactsState { 32 | @override 33 | String toString() => 'AddContactProgressState'; 34 | } 35 | 36 | // Add contact success 37 | class AddContactSuccessState extends ContactsState { 38 | @override 39 | String toString() => 'AddContactSuccessState'; 40 | } 41 | 42 | // Add contact failed 43 | class AddContactFailedState extends ContactsState { 44 | final MessioException exception; 45 | AddContactFailedState(this.exception): super([exception]); 46 | @override 47 | String toString() => 'AddContactFailedState'; 48 | } 49 | 50 | 51 | // Handle errors 52 | class ErrorState extends ContactsState { 53 | final MessioException exception; 54 | ErrorState(this.exception): super([exception]); 55 | @override 56 | String toString() => 'ErrorState'; 57 | } 58 | -------------------------------------------------------------------------------- /lib/blocs/home/bloc.dart: -------------------------------------------------------------------------------- 1 | export 'home_bloc.dart'; 2 | export 'home_event.dart'; 3 | export 'home_state.dart'; -------------------------------------------------------------------------------- /lib/blocs/home/home_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bloc/bloc.dart'; 3 | import 'package:messio/repositories/chat_repository.dart'; 4 | import './bloc.dart'; 5 | 6 | class HomeBloc extends Bloc { 7 | ChatRepository chatRepository; 8 | 9 | HomeBloc({this.chatRepository}):super(InitialHomeState()){ 10 | assert(chatRepository != null); 11 | on(mapFetchHomeChatsEventToState); 12 | on(mapReceivedChatsEventToState); 13 | } 14 | 15 | Future mapReceivedChatsEventToState(ReceivedChatsEvent event, Emitter emit) async { 16 | emit(FetchingHomeChatsState()); 17 | emit(FetchedHomeChatsState(event.conversations)); 18 | } 19 | Future mapFetchHomeChatsEventToState(FetchHomeChatsEvent event, Emitter emit) async { 20 | emit(FetchingHomeChatsState()); 21 | chatRepository.getConversations().listen( 22 | (conversations) => add(ReceivedChatsEvent(conversations))); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/blocs/home/home_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:messio/models/conversation.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | @immutable 6 | abstract class HomeEvent extends Equatable { 7 | HomeEvent([List props = const []]); 8 | @override 9 | List get props => []; 10 | } 11 | class FetchHomeChatsEvent extends HomeEvent{ 12 | @override 13 | String toString() => 'FetchHomeChatsEvent'; 14 | } 15 | class ReceivedChatsEvent extends HomeEvent{ 16 | final List conversations; 17 | ReceivedChatsEvent(this.conversations); 18 | 19 | @override 20 | String toString() => 'ReceivedChatsEvent'; 21 | 22 | } -------------------------------------------------------------------------------- /lib/blocs/home/home_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:messio/models/conversation.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | @immutable 6 | abstract class HomeState extends Equatable { 7 | HomeState([List props = const []]); 8 | @override 9 | List get props => []; 10 | } 11 | 12 | class InitialHomeState extends HomeState {} 13 | 14 | class FetchingHomeChatsState extends HomeState{ 15 | @override 16 | String toString() => 'FetchingHomeChatsState'; 17 | } 18 | class FetchedHomeChatsState extends HomeState{ 19 | final List conversations; 20 | 21 | FetchedHomeChatsState(this.conversations); 22 | 23 | @override 24 | String toString() => 'FetchedHomeChatsState'; 25 | } 26 | -------------------------------------------------------------------------------- /lib/config/assets.dart: -------------------------------------------------------------------------------- 1 | class Assets{ 2 | static const String user = 'assets/user.png'; 3 | static const String app_icon = 'assets/launcher/ic_launcher.png'; 4 | static const String app_icon_fg = 'assets/launcher/ic_foreground.png'; 5 | static const String app_icon_bg = 'assets/launcher/ic_background.png'; 6 | static const String google_button = 'assets/google.png'; 7 | static const String social = 'assets/social.png'; 8 | static const String placeholder = 'assets/placeholder.png'; 9 | 10 | } -------------------------------------------------------------------------------- /lib/config/constants.dart: -------------------------------------------------------------------------------- 1 | class Constants{ 2 | static const firstRun = "firstRun"; 3 | static const sessionUid = "sessionUid"; 4 | static const sessionUsername = 'sessionUsername'; 5 | static const sessionName = 'sessionName'; 6 | static const sessionProfilePictureUrl = 'sessionProfilePictureUrl'; 7 | 8 | static const configDarkMode = 'configDarkMode'; 9 | static const configMessagePeek = 'configMessagePeek'; 10 | static const configMessagePaging = 'configMessagePaging'; 11 | static const configImageCompression = 'configImageCompression'; 12 | static String downloadsDirPath; 13 | static String cacheDirPath; 14 | } -------------------------------------------------------------------------------- /lib/config/decorations.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'palette.dart'; 4 | 5 | class Decorations { 6 | 7 | static InputDecoration getInputDecoration({@required String hint, @required BuildContext context}) { 8 | return InputDecoration( 9 | hintText: hint, 10 | hintStyle: TextStyle(color:Theme.of(context).hintColor), 11 | contentPadding: EdgeInsets.fromLTRB(10, 5, 10, 5), 12 | focusedBorder: OutlineInputBorder( 13 | borderSide: BorderSide( 14 | color: Theme.of(context).hintColor, 15 | width: 0.1), 16 | ), 17 | enabledBorder: OutlineInputBorder( 18 | borderSide: BorderSide( 19 | color: Theme.of(context).hintColor, 20 | width: 0.1), 21 | ), 22 | ); 23 | } 24 | 25 | static InputDecoration getInputDecorationLight({@required String hint, @required BuildContext context}) { 26 | return InputDecoration( 27 | hintText: hint, 28 | hintStyle: TextStyle(color:Theme.of(context).hintColor), 29 | contentPadding: EdgeInsets.fromLTRB(10, 5, 10, 5), 30 | focusedBorder: OutlineInputBorder( 31 | borderSide: BorderSide( 32 | color: Palette.primaryColor, 33 | width: 0.1), 34 | ), 35 | enabledBorder: OutlineInputBorder( 36 | borderSide: BorderSide( 37 | color: Palette.primaryColor, 38 | width: 0.1), 39 | ), 40 | ); 41 | } 42 | } -------------------------------------------------------------------------------- /lib/config/palette.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/material.dart'; 3 | 4 | // Color palette for the unthemed pages 5 | class Palette{ 6 | static Color primaryColor = Colors.white; 7 | static Color accentColor = Color(0xff4fc3f7); 8 | static Color secondaryColor = Colors.black; 9 | 10 | static Color gradientStartColor = accentColor; 11 | static Color gradientEndColor = Color(0xff6aa8fd); 12 | static Color errorGradientStartColor = Color(0xffd50000); 13 | static Color errorGradientEndColor = Color(0xff9b0000); 14 | 15 | 16 | static Color primaryTextColorLight = Colors.white; 17 | static Color secondaryTextColorLight = Colors.white70; 18 | static Color hintTextColorLight = Colors.white70; 19 | 20 | 21 | static Color selfMessageBackgroundColor = Color(0xff4fc3f7); 22 | static Color otherMessageBackgroundColor = Colors.white; 23 | 24 | static Color selfMessageColor = Colors.white; 25 | static Color otherMessageColor = Color(0xff3f3f3f); 26 | 27 | static Color greyColor = Colors.grey; 28 | } -------------------------------------------------------------------------------- /lib/config/paths.dart: -------------------------------------------------------------------------------- 1 | import 'package:file_picker/file_picker.dart'; 2 | 3 | class Paths{ 4 | 5 | /* 6 | Firebase paths 7 | */ 8 | static const String profilePicturePath = 'profile_pictures'; 9 | static const String imageAttachmentsPath = 'images'; 10 | static const String videoAttachmentsPath = 'videos'; 11 | static const String fileAttachmentsPath = 'files'; 12 | static const String usersPath = '/users'; 13 | static const String contactsPath = 'contacts'; 14 | static const String usernameUidMapPath = '/username_uid_map'; 15 | static const String chatsPath = '/chats'; 16 | static const String chat_messages = '/chat_messages'; 17 | static const String messagesPath = 'messages'; 18 | 19 | static String getAttachmentPathByFileType(FileType fileType){ 20 | if(fileType == FileType.image) 21 | return imageAttachmentsPath; 22 | else if(fileType == FileType.video) 23 | return videoAttachmentsPath; 24 | else 25 | return fileAttachmentsPath; 26 | } 27 | } -------------------------------------------------------------------------------- /lib/config/styles.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'palette.dart'; 4 | 5 | class Styles{ 6 | 7 | static TextStyle numberPickerHeading = TextStyle( 8 | fontSize: 30, 9 | color: Palette.primaryTextColorLight 10 | ); 11 | 12 | static TextStyle questionLight = TextStyle( 13 | color: Palette.primaryTextColorLight, 14 | fontSize: 16); 15 | 16 | static TextStyle subHeadingLight = TextStyle( 17 | color: Palette.primaryTextColorLight, 18 | fontSize: 14); 19 | 20 | static TextStyle textLight = TextStyle( 21 | color: Palette.secondaryTextColorLight 22 | ); 23 | 24 | } -------------------------------------------------------------------------------- /lib/config/themes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'palette.dart'; 4 | class Themes{ 5 | static final ThemeData light = ThemeData( 6 | accentColor: Palette.accentColor, 7 | primaryColor: Colors.white, 8 | primarySwatch: Colors.blue, 9 | disabledColor: Colors.grey, 10 | cardColor: Colors.white, 11 | canvasColor: Colors.grey[50], 12 | scaffoldBackgroundColor: Colors.white, 13 | brightness: Brightness.light, 14 | primaryColorBrightness: Brightness.light, 15 | backgroundColor:Colors.white, 16 | buttonColor: Palette.accentColor, 17 | appBarTheme: AppBarTheme(elevation: 0.0), 18 | fontFamily: 'Manrope', 19 | bottomSheetTheme: BottomSheetThemeData( 20 | backgroundColor: Colors.black.withOpacity(0)) 21 | ); 22 | static final dark = ThemeData( 23 | accentColor: Palette.accentColor, 24 | primaryColor: Colors.black, 25 | primarySwatch: Colors.blue, 26 | disabledColor: Colors.grey, 27 | cardColor: Color(0xff191919), 28 | canvasColor: Colors.grey[50], 29 | backgroundColor:Color(0xff191919), 30 | scaffoldBackgroundColor: Colors.black, 31 | brightness: Brightness.dark, 32 | primaryColorBrightness: Brightness.dark, 33 | buttonColor: Palette.accentColor, 34 | appBarTheme: AppBarTheme(elevation: 0.0), 35 | fontFamily: 'Manrope', 36 | bottomSheetTheme: BottomSheetThemeData( 37 | backgroundColor: Colors.black.withOpacity(0)) 38 | ); 39 | 40 | } -------------------------------------------------------------------------------- /lib/config/transitions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | class SlideLeftRoute extends PageRouteBuilder { 3 | final Widget page; 4 | SlideLeftRoute({this.page}) 5 | : super( 6 | pageBuilder: ( 7 | BuildContext context, 8 | Animation animation, 9 | Animation secondaryAnimation, 10 | ) => 11 | page, 12 | transitionsBuilder: ( 13 | BuildContext context, 14 | Animation animation, 15 | Animation secondaryAnimation, 16 | Widget child, 17 | ) => 18 | SlideTransition( 19 | position: Tween( 20 | begin: const Offset(0, 1), 21 | end: Offset.zero, 22 | ).animate(animation), 23 | child: child, 24 | ), 25 | ); 26 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:downloads_path_provider/downloads_path_provider.dart'; 3 | import 'package:firebase_core/firebase_core.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:messio/blocs/attachments/attachments_bloc.dart'; 7 | import 'package:messio/blocs/chats/bloc.dart'; 8 | import 'package:messio/blocs/config/bloc.dart'; 9 | import 'package:messio/blocs/contacts/bloc.dart'; 10 | import 'package:messio/blocs/home/bloc.dart'; 11 | import 'package:messio/config/constants.dart'; 12 | import 'package:messio/config/themes.dart'; 13 | import 'package:messio/pages/home_page.dart'; 14 | import 'package:messio/repositories/authentication_repository.dart'; 15 | import 'package:messio/repositories/chat_repository.dart'; 16 | import 'package:messio/repositories/storage_repository.dart'; 17 | import 'package:messio/repositories/user_data_repository.dart'; 18 | import 'package:messio/utils/shared_objects.dart'; 19 | import 'package:path_provider/path_provider.dart'; 20 | import 'blocs/authentication/bloc.dart'; 21 | import 'pages/register_page.dart'; 22 | void main() async { 23 | WidgetsFlutterBinding.ensureInitialized(); 24 | 25 | await Firebase.initializeApp(); 26 | //create instances of the repositories to supply them to the app 27 | final AuthenticationRepository authRepository = AuthenticationRepository(); 28 | final UserDataRepository userDataRepository = UserDataRepository(); 29 | final StorageRepository storageRepository = StorageRepository(); 30 | final ChatRepository chatRepository = ChatRepository(); 31 | SharedObjects.prefs = await CachedSharedPreferences.getInstance(); 32 | Constants.cacheDirPath = (await getTemporaryDirectory()).path; 33 | Constants.downloadsDirPath = 34 | (await DownloadsPathProvider.downloadsDirectory).path; 35 | runApp(MultiBlocProvider( 36 | providers: [ 37 | BlocProvider( 38 | create: (context) => AuthenticationBloc( 39 | authenticationRepository: authRepository, 40 | userDataRepository: userDataRepository, 41 | storageRepository: storageRepository) 42 | ..add(AppLaunched()), 43 | ), 44 | BlocProvider( 45 | create: (context) => ContactsBloc( 46 | userDataRepository: userDataRepository, 47 | chatRepository: chatRepository), 48 | ), 49 | BlocProvider( 50 | create: (context) => ChatBloc( 51 | userDataRepository: userDataRepository, 52 | storageRepository: storageRepository, 53 | chatRepository: chatRepository), 54 | ), 55 | BlocProvider( 56 | create: (context) => AttachmentsBloc(chatRepository: chatRepository), 57 | ), 58 | BlocProvider( 59 | create: (context) => HomeBloc(chatRepository: chatRepository), 60 | ), 61 | BlocProvider( 62 | create: (context) => ConfigBloc(storageRepository: storageRepository,userDataRepository: userDataRepository), 63 | ) 64 | ], 65 | child: Messio(), 66 | )); 67 | } 68 | 69 | 70 | 71 | class Messio extends StatefulWidget { 72 | @override 73 | _MessioState createState() => _MessioState(); 74 | } 75 | 76 | class _MessioState extends State { 77 | ThemeData theme; 78 | Key key = UniqueKey(); 79 | @override 80 | Widget build(BuildContext context) { 81 | return BlocBuilder(builder: (context, state) { 82 | if (state is UnConfigState) { 83 | theme = SharedObjects.prefs.getBool(Constants.configDarkMode) 84 | ? Themes.dark 85 | : Themes.light; 86 | } 87 | if(state is RestartedAppState){ 88 | key = UniqueKey(); 89 | } 90 | if (state is ConfigChangeState && state.key == Constants.configDarkMode) { 91 | theme = state.value ? Themes.dark : Themes.light; 92 | } 93 | return MaterialApp( 94 | title: 'Messio', 95 | theme: theme, 96 | key: key, 97 | debugShowCheckedModeBanner: false, 98 | home: BlocBuilder( 99 | builder: (context, state) { 100 | if (state is UnAuthenticated) { 101 | return RegisterPage(); 102 | } else if (state is ProfileUpdated) { 103 | if(SharedObjects.prefs.getBool(Constants.configMessagePaging)) 104 | BlocProvider.of(context).add(FetchChatListEvent()); 105 | return HomePage(); 106 | } else { 107 | return RegisterPage(); 108 | } 109 | }, 110 | ), 111 | ); 112 | }); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/models/chat.dart: -------------------------------------------------------------------------------- 1 | 2 | class Chat{ 3 | String username; 4 | String chatId; 5 | Chat(this.username,this.chatId); 6 | @override 7 | String toString() => '{ username= $username, chatId = $chatId}'; 8 | } -------------------------------------------------------------------------------- /lib/models/contact.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:messio/models/conversation.dart'; 3 | import 'package:messio/utils/document_snapshot_extension.dart'; 4 | 5 | class Contact { 6 | String username; 7 | String name; 8 | String photoUrl; 9 | String documentId; 10 | String chatId; 11 | Contact(this.documentId, this.username, this.name,this.photoUrl, this.chatId); 12 | 13 | factory Contact.fromFirestore(DocumentSnapshot doc) { 14 | Map data = doc.dataAsMap; 15 | return Contact(doc.id, data['username'], data['name'],data['photoUrl'], data['chatId']); 16 | } 17 | 18 | @override 19 | String toString() { 20 | return '{ documentId: $documentId, name: $name, username: $username, photoUrl: $photoUrl , chatId: $chatId}'; 21 | } 22 | 23 | String getFirstName() => name.split(' ')[0]; 24 | 25 | String getLastName() { 26 | List names = name.split(' '); 27 | return names.length > 1 ? names[1] : ''; 28 | } 29 | 30 | factory Contact.fromConversation(Conversation conversation) { 31 | return Contact(conversation.user.documentId, conversation.user.username, 32 | conversation.user.name,conversation.user.photoUrl,conversation.chatId,); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/models/conversation.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:messio/config/constants.dart'; 3 | import 'package:messio/models/message.dart'; 4 | import 'package:messio/models/messio_user.dart'; 5 | import 'package:messio/utils/shared_objects.dart'; 6 | import 'package:messio/utils/document_snapshot_extension.dart'; 7 | 8 | class Conversation { 9 | String chatId; 10 | MessioUser user; 11 | Message latestMessage; 12 | 13 | Conversation(this.chatId, this.user, this.latestMessage); 14 | 15 | factory Conversation.fromFireStore(DocumentSnapshot doc) { 16 | Map data = doc.dataAsMap; 17 | List members = List.from(data['members']); 18 | String selfUsername = 19 | SharedObjects.prefs.getString(Constants.sessionUsername); 20 | MessioUser contact; 21 | for (int i = 0; i < members.length; i++) { 22 | if (members[i] != selfUsername) { 23 | final userDetails = Map.from((data['membersData'])[i]); 24 | contact = MessioUser.fromMap(userDetails); 25 | } 26 | } 27 | return Conversation( 28 | doc.id, contact, Message.fromMap(Map.from(data['latestMessage']))); 29 | } 30 | 31 | @override 32 | String toString() => 33 | '{ user= $user, chatId = $chatId, latestMessage = $latestMessage}'; 34 | } 35 | -------------------------------------------------------------------------------- /lib/models/message.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:messio/config/constants.dart'; 3 | import 'package:messio/utils/shared_objects.dart'; 4 | import 'package:messio/utils/document_snapshot_extension.dart'; 5 | 6 | 7 | abstract class Message { 8 | int timeStamp; 9 | String senderName; 10 | String senderUsername; 11 | bool isSelf; 12 | String documentId; 13 | Message(this.timeStamp, this.senderName, this.senderUsername); 14 | 15 | factory Message.fromFireStore(DocumentSnapshot doc) { 16 | final int type = doc.dataAsMap['type']; 17 | Message message; 18 | switch (type) { 19 | case 0: 20 | message = TextMessage.fromFirestore(doc); 21 | break; 22 | case 1: 23 | message = ImageMessage.fromFirestore(doc); 24 | break; 25 | case 2: 26 | message = VideoMessage.fromFirestore(doc); 27 | break; 28 | case 3: 29 | message = FileMessage.fromFirestore(doc); 30 | } 31 | message.isSelf = SharedObjects.prefs.getString(Constants.sessionUsername) == 32 | message.senderUsername; 33 | message.documentId = doc.id; 34 | return message; 35 | } 36 | 37 | factory Message.fromMap(Map map) { 38 | final int type = map['type']; 39 | Message message; 40 | switch (type) { 41 | case 0: 42 | message = TextMessage.fromMap(map); 43 | break; 44 | case 1: 45 | message = ImageMessage.fromMap(map); 46 | break; 47 | case 2: 48 | message = VideoMessage.fromMap(map); 49 | break; 50 | case 3: 51 | message = FileMessage.fromMap(map); 52 | } 53 | message.isSelf = SharedObjects.prefs.getString(Constants.sessionUsername) == 54 | message.senderUsername; 55 | return message; 56 | } 57 | Map toMap(); 58 | } 59 | 60 | class TextMessage extends Message { 61 | String text; 62 | TextMessage(this.text, timeStamp, senderName, senderUsername) 63 | : super(timeStamp, senderName, senderUsername); 64 | 65 | factory TextMessage.fromFirestore(DocumentSnapshot doc) { 66 | Map data = doc.dataAsMap; 67 | return TextMessage.fromMap(data); 68 | } 69 | factory TextMessage.fromMap(Map data) { 70 | return TextMessage(data['text'], data['timeStamp'], data['senderName'], 71 | data['senderUsername']); 72 | } 73 | @override 74 | Map toMap() { 75 | Map map = Map(); 76 | map['text'] = text; 77 | map['timeStamp'] = timeStamp; 78 | map['senderName'] = senderName; 79 | map['senderUsername'] = senderUsername; 80 | map['type'] = 0; 81 | return map; 82 | } 83 | 84 | @override 85 | String toString() => '{ senderName : $senderName, senderUsername : $senderUsername, isSelf : $isSelf , timeStamp : $timeStamp, type : 3, text: $text }'; 86 | 87 | 88 | } 89 | 90 | class ImageMessage extends Message { 91 | String imageUrl; 92 | String fileName; 93 | ImageMessage(this.imageUrl,this.fileName, timeStamp, senderName, senderUsername) 94 | : super(timeStamp, senderName, senderUsername); 95 | 96 | factory ImageMessage.fromFirestore(DocumentSnapshot doc) { 97 | Map data = doc.dataAsMap; 98 | return ImageMessage.fromMap(data); 99 | } 100 | factory ImageMessage.fromMap(Map data) { 101 | return ImageMessage(data['imageUrl'],data['fileName'], data['timeStamp'], data['senderName'], 102 | data['senderUsername']); 103 | } 104 | 105 | @override 106 | Map toMap() { 107 | Map map = Map(); 108 | map['imageUrl'] = imageUrl; 109 | map['fileName']= fileName; 110 | map['timeStamp'] = timeStamp; 111 | map['senderName'] = senderName; 112 | map['senderUsername'] = senderUsername; 113 | map['type'] = 1; 114 | print('map $map'); 115 | return map; 116 | } 117 | @override 118 | String toString() => '{ senderName : $senderName, senderUsername : $senderUsername, isSelf : $isSelf , timeStamp : $timeStamp, type : 3, fileName: $fileName, imageUrl : $imageUrl }'; 119 | 120 | } 121 | 122 | 123 | class VideoMessage extends Message { 124 | String videoUrl; 125 | String fileName; 126 | VideoMessage(this.videoUrl,this.fileName, timeStamp, senderName, senderUsername) 127 | : super(timeStamp, senderName, senderUsername); 128 | 129 | factory VideoMessage.fromFirestore(DocumentSnapshot doc) { 130 | Map data = doc.dataAsMap; 131 | return VideoMessage.fromMap(data); 132 | } 133 | factory VideoMessage.fromMap(Map data) { 134 | return VideoMessage(data['videoUrl'],data['fileName'], data['timeStamp'], data['senderName'], 135 | data['senderUsername']); 136 | } 137 | 138 | @override 139 | Map toMap() { 140 | Map map = Map(); 141 | map['videoUrl'] = videoUrl; 142 | map['fileName']= fileName; 143 | map['timeStamp'] = timeStamp; 144 | map['senderName'] = senderName; 145 | map['senderUsername'] = senderUsername; 146 | map['type'] = 2; 147 | return map; 148 | } 149 | @override 150 | String toString() => '{ senderName : $senderName, senderUsername : $senderUsername, isSelf : $isSelf , timeStamp : $timeStamp, type : 3, fileName: $fileName, videoUrl : $videoUrl }'; 151 | 152 | 153 | } 154 | 155 | class FileMessage extends Message { 156 | String fileUrl; 157 | String fileName; 158 | FileMessage(this.fileUrl,this.fileName, timeStamp, senderName, senderUsername) 159 | : super(timeStamp, senderName, senderUsername); 160 | 161 | factory FileMessage.fromFirestore(DocumentSnapshot doc) { 162 | Map data = doc.dataAsMap; 163 | return FileMessage.fromMap(data); 164 | } 165 | factory FileMessage.fromMap(Map data) { 166 | return FileMessage(data['fileUrl'],data['fileName'], data['timeStamp'], data['senderName'], 167 | data['senderUsername']); 168 | } 169 | 170 | @override 171 | Map toMap() { 172 | Map map = Map(); 173 | map['fileUrl'] = fileUrl; 174 | map['fileName']= fileName; 175 | map['timeStamp'] = timeStamp; 176 | map['senderName'] = senderName; 177 | map['senderUsername'] = senderUsername; 178 | map['type'] = 3; 179 | return map; 180 | } 181 | 182 | @override 183 | String toString() => '{ senderName : $senderName, senderUsername : $senderUsername, isSelf : $isSelf , timeStamp : $timeStamp, type : 3, fileName: $fileName, fileUrl : $fileUrl }'; 184 | 185 | } 186 | -------------------------------------------------------------------------------- /lib/models/messio_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:messio/utils/document_snapshot_extension.dart'; 3 | 4 | class MessioUser{ 5 | String documentId; 6 | String name; 7 | String username; 8 | int age; 9 | String photoUrl; 10 | 11 | MessioUser({this.documentId, this.name, this.username, this.age, this.photoUrl}); 12 | 13 | factory MessioUser.fromFirestore(DocumentSnapshot doc) { 14 | Map data = doc.dataAsMap; 15 | return MessioUser( 16 | documentId: doc.id, 17 | name: data['name'], 18 | username: data['username'], 19 | age: data['age'], 20 | photoUrl: data['photoUrl'] 21 | ); 22 | } 23 | factory MessioUser.fromMap(Map data) { 24 | return MessioUser( 25 | documentId: data['uid'], 26 | name: data['name'], 27 | username: data['username'], 28 | age: data['age'], 29 | photoUrl: data['photoUrl'] 30 | ); 31 | } 32 | @override 33 | String toString() { 34 | return '{ documentId: $documentId, name: $name, age: $age, username: $username, photoUrl: $photoUrl }'; 35 | } 36 | } -------------------------------------------------------------------------------- /lib/models/video_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'message.dart'; 4 | 5 | class VideoWrapper{ 6 | final File file; //thumbnail for the video 7 | final VideoMessage videoMessage; 8 | VideoWrapper(this.file, this.videoMessage); 9 | 10 | } -------------------------------------------------------------------------------- /lib/pages/conversation_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/rendering.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:messio/blocs/chats/bloc.dart'; 7 | import 'package:messio/models/chat.dart'; 8 | import 'package:messio/models/contact.dart'; 9 | import 'package:messio/widgets/chat_app_bar.dart'; 10 | import 'package:messio/widgets/chat_list_widget.dart'; 11 | 12 | // ignore: must_be_immutable 13 | class ConversationPage extends StatefulWidget { 14 | Chat chat; 15 | Contact contact; 16 | @override 17 | _ConversationPageState createState() => _ConversationPageState(chat,contact); 18 | 19 | ConversationPage({this.chat,this.contact}); 20 | } 21 | 22 | class _ConversationPageState extends State with AutomaticKeepAliveClientMixin{ 23 | Chat chat; 24 | Contact contact; 25 | _ConversationPageState(this.chat,this.contact); 26 | 27 | ChatBloc chatBloc; 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | if(contact!=null) 33 | chat = Chat(contact.username,contact.chatId); 34 | chatBloc = BlocProvider.of(context); 35 | chatBloc.add(FetchConversationDetailsEvent(chat)); 36 | } 37 | @override 38 | Widget build(BuildContext context) { 39 | super.build(context); 40 | return Stack( 41 | children: [ 42 | Container( 43 | margin: EdgeInsets.only(top: 100), 44 | color: Theme.of(context).backgroundColor, 45 | child: ChatListWidget(chat), 46 | ), 47 | SizedBox.fromSize( 48 | size: Size.fromHeight(100), 49 | child: ChatAppBar(contact: contact,chat: chat) 50 | ) 51 | ], 52 | ); 53 | } 54 | 55 | @override 56 | bool get wantKeepAlive => true; 57 | 58 | } 59 | -------------------------------------------------------------------------------- /lib/pages/conversation_page_slide.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:messio/blocs/chats/bloc.dart'; 4 | import 'package:messio/blocs/config/bloc.dart'; 5 | import 'package:messio/config/constants.dart'; 6 | import 'package:messio/models/chat.dart'; 7 | import 'package:messio/models/contact.dart'; 8 | import 'package:messio/utils/shared_objects.dart'; 9 | import 'package:messio/widgets/bottom_sheet_fixed.dart'; 10 | import 'package:messio/widgets/input_widget.dart'; 11 | import '../widgets/conversation_bottom_sheet.dart'; 12 | import 'conversation_page.dart'; 13 | 14 | class ConversationPageSlide extends StatefulWidget { 15 | final Contact startContact; 16 | 17 | @override 18 | _ConversationPageSlideState createState() => 19 | _ConversationPageSlideState(startContact); 20 | 21 | const ConversationPageSlide({this.startContact}); 22 | } 23 | 24 | class _ConversationPageSlideState extends State 25 | with SingleTickerProviderStateMixin { 26 | PageController pageController = PageController(); 27 | final GlobalKey _scaffoldKey = GlobalKey(); 28 | final Contact startContact; 29 | ChatBloc chatBloc; 30 | List chatList = List(); 31 | bool isFirstLaunch = true; 32 | bool configMessagePeek = true; 33 | _ConversationPageSlideState(this.startContact); 34 | 35 | @override 36 | void initState() { 37 | chatBloc = BlocProvider.of(context); 38 | // chatBloc.dispatch(FetchChatListEvent()); 39 | super.initState(); 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return SafeArea( 45 | child: Scaffold( 46 | key: _scaffoldKey, 47 | body: Column( 48 | children: [ 49 | BlocListener( 50 | bloc: chatBloc, 51 | listener: (bc, state) { 52 | if (isFirstLaunch && chatList.isNotEmpty) { 53 | isFirstLaunch = false; 54 | for (int i = 0; i < chatList.length; i++) { 55 | if (startContact.username == chatList[i].username) { 56 | BlocProvider.of(context) 57 | .add(PageChangedEvent(i, chatList[i])); 58 | pageController.jumpToPage(i); 59 | } 60 | } 61 | } 62 | }, 63 | child: Expanded(child: BlocBuilder( 64 | builder: (context, state) { 65 | if (state is FetchedChatListState) 66 | chatList = state.chatList; 67 | return PageView.builder( 68 | controller: pageController, 69 | itemCount: chatList.length, 70 | onPageChanged: (index) => 71 | BlocProvider.of(context).add( 72 | PageChangedEvent(index, chatList[index])), 73 | itemBuilder: (bc, index) => 74 | ConversationPage(chat:chatList[index])); 75 | }, 76 | ))), 77 | BlocBuilder( 78 | builder: (context, state) { 79 | if(state is UnConfigState) 80 | configMessagePeek = SharedObjects.prefs.getBool(Constants.configMessagePeek); 81 | if(state is ConfigChangeState) 82 | if(state.key == Constants.configMessagePeek) configMessagePeek = state.value; 83 | return GestureDetector( 84 | child: InputWidget(), 85 | onPanUpdate: (details) { 86 | if(!configMessagePeek) 87 | return; 88 | if (details.delta.dy < 100) { 89 | showModalBottomSheetApp(context: context, builder: (context)=>ConversationBottomSheet()); 90 | } 91 | }); 92 | } 93 | ) 94 | ], 95 | ), 96 | )); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/pages/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:messio/blocs/home/bloc.dart'; 4 | import 'package:messio/config/transitions.dart'; 5 | import 'package:messio/models/conversation.dart'; 6 | import 'package:messio/pages/settings_page.dart'; 7 | import 'package:messio/widgets/chat_row_widget.dart'; 8 | import 'package:messio/widgets/gradient_fab.dart'; 9 | 10 | import 'contact_list_page.dart'; 11 | 12 | class HomePage extends StatefulWidget { 13 | @override 14 | _HomePageState createState() => _HomePageState(); 15 | } 16 | 17 | class _HomePageState extends State { 18 | HomeBloc homeBloc; 19 | List conversations = List(); 20 | 21 | @override 22 | void initState() { 23 | homeBloc = BlocProvider.of(context); 24 | homeBloc.add(FetchHomeChatsEvent()); 25 | super.initState(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return SafeArea( 31 | child: Scaffold( 32 | body: CustomScrollView(slivers: [ 33 | SliverAppBar( 34 | expandedHeight: 180.0, 35 | pinned: true, 36 | elevation: 0, 37 | centerTitle: true, 38 | actions: [ 39 | IconButton( 40 | icon: Icon(Icons.settings), 41 | color: Theme.of(context).accentColor, 42 | onPressed: (){ 43 | Navigator.push(context, SlideLeftRoute(page:SettingsPage())); 44 | }, 45 | ) 46 | ], 47 | flexibleSpace: FlexibleSpaceBar( 48 | centerTitle: true, 49 | title: Text("Chats", style: Theme.of(context).textTheme.headline6), 50 | ), 51 | ), 52 | BlocBuilder(builder: (context, state) { 53 | if (state is FetchingHomeChatsState) { 54 | return SliverToBoxAdapter( 55 | child: SizedBox( 56 | height: (MediaQuery.of(context).size.height), 57 | child: Center(child: CircularProgressIndicator()), 58 | ), 59 | ); 60 | } else if (state is FetchedHomeChatsState) { 61 | conversations = state.conversations; 62 | } 63 | return SliverList( 64 | delegate: SliverChildBuilderDelegate( 65 | (context, index) => ChatRowWidget(conversations[index]), 66 | childCount: conversations.length), 67 | ); 68 | }) 69 | ]), 70 | floatingActionButton: GradientFab( 71 | child: Icon(Icons.contacts, color: Theme.of(context).primaryColor,), 72 | onPressed: () => Navigator.push( 73 | context, SlideLeftRoute(page: ContactListPage())), 74 | ))); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/pages/single_conversation_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:messio/blocs/chats/bloc.dart'; 4 | import 'package:messio/blocs/config/bloc.dart'; 5 | import 'package:messio/config/constants.dart'; 6 | import 'package:messio/models/contact.dart'; 7 | import 'package:messio/utils/shared_objects.dart'; 8 | import 'package:messio/widgets/bottom_sheet_fixed.dart'; 9 | import 'package:messio/widgets/input_widget.dart'; 10 | import '../widgets/conversation_bottom_sheet.dart'; 11 | import 'conversation_page.dart'; 12 | 13 | class SingleConversationPage extends StatefulWidget { 14 | final Contact contact; 15 | @override 16 | _SingleConversationPageState createState() => 17 | _SingleConversationPageState(contact); 18 | 19 | const SingleConversationPage({this.contact}); 20 | } 21 | 22 | class _SingleConversationPageState extends State 23 | with SingleTickerProviderStateMixin { 24 | final GlobalKey _scaffoldKey = GlobalKey(); 25 | final Contact contact; 26 | ChatBloc chatBloc; 27 | bool isFirstLaunch = true; 28 | bool configMessagePeek = true; 29 | _SingleConversationPageState(this.contact); 30 | 31 | @override 32 | void initState() { 33 | chatBloc = BlocProvider.of(context); 34 | chatBloc.add(RegisterActiveChatEvent(contact.chatId)); 35 | super.initState(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return SafeArea( 41 | child: Scaffold( 42 | key: _scaffoldKey, 43 | body: Column( 44 | children: [ 45 | Expanded(child: ConversationPage(contact: contact,)), 46 | BlocBuilder( 47 | builder: (context, state) { 48 | if(state is UnConfigState) 49 | configMessagePeek = SharedObjects.prefs.getBool(Constants.configMessagePeek); 50 | if(state is ConfigChangeState) 51 | if(state.key == Constants.configMessagePeek) configMessagePeek = state.value; 52 | return GestureDetector( 53 | child: InputWidget(), 54 | onPanUpdate: (details) { 55 | if(!configMessagePeek) 56 | return; 57 | if (details.delta.dy < 100) { 58 | showModalBottomSheetApp(context: context, builder: (context)=>ConversationBottomSheet()); 59 | } 60 | }); 61 | } 62 | ) 63 | ], 64 | ), 65 | )); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/providers/authentication_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:google_sign_in/google_sign_in.dart'; 3 | import 'package:messio/config/constants.dart'; 4 | import 'package:messio/utils/shared_objects.dart'; 5 | import 'base_providers.dart'; 6 | 7 | class AuthenticationProvider extends BaseAuthenticationProvider { 8 | 9 | final FirebaseAuth firebaseAuth; 10 | final GoogleSignIn googleSignIn; 11 | 12 | AuthenticationProvider({FirebaseAuth firebaseAuth, GoogleSignIn googleSignIn}): 13 | firebaseAuth= firebaseAuth ?? FirebaseAuth.instance, googleSignIn = googleSignIn ?? GoogleSignIn(); 14 | 15 | @override 16 | Future signInWithGoogle() async { 17 | final GoogleSignInAccount account = 18 | await googleSignIn.signIn(); //show the goggle login prompt 19 | final GoogleSignInAuthentication authentication = 20 | await account.authentication; //get the authentication object 21 | final AuthCredential credential = GoogleAuthProvider.credential( 22 | //retreive the authentication credentials 23 | idToken: authentication.idToken, 24 | accessToken: authentication.accessToken); 25 | await firebaseAuth.signInWithCredential( 26 | credential); //sign in to firebase using the generated credentials 27 | final firebaseUser = firebaseAuth.currentUser; 28 | await SharedObjects.prefs.setString(Constants.sessionUid, firebaseUser.uid); 29 | print('Session UID1 ${SharedObjects.prefs.getString(Constants.sessionUid)}'); 30 | return firebaseUser; //return the firebase user created 31 | } 32 | 33 | @override 34 | Future signOutUser() async { 35 | print('firebaseauth $firebaseAuth'); 36 | await SharedObjects.prefs.clearSession(); 37 | await Future.wait([firebaseAuth.signOut(), googleSignIn.signOut()]); // terminate the session 38 | } 39 | 40 | @override 41 | User getCurrentUser() { 42 | return firebaseAuth.currentUser; //retrieve the current user 43 | } 44 | 45 | @override 46 | bool isLoggedIn() { 47 | final user = firebaseAuth.currentUser; //check if user is logged in or not 48 | return user != null; 49 | } 50 | 51 | @override 52 | void dispose() {} 53 | } 54 | -------------------------------------------------------------------------------- /lib/providers/base_providers.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:messio/models/chat.dart'; 4 | import 'package:messio/models/contact.dart'; 5 | import 'package:messio/models/conversation.dart'; 6 | import 'package:messio/models/message.dart'; 7 | import 'package:messio/models/messio_user.dart'; 8 | 9 | abstract class BaseProvider{ 10 | void dispose(); 11 | } 12 | abstract class BaseAuthenticationProvider extends BaseProvider{ 13 | Future signInWithGoogle(); 14 | Future signOutUser(); 15 | User getCurrentUser(); 16 | bool isLoggedIn(); 17 | } 18 | 19 | abstract class BaseUserDataProvider extends BaseProvider{ 20 | Future saveDetailsFromGoogleAuth(User user); 21 | Future saveProfileDetails(String profileImageUrl, int age, String username); 22 | Future isProfileComplete(); 23 | Stream> getContacts(); 24 | Future addContact(String username); 25 | Future getUser(String username); 26 | Future getUidByUsername(String username); 27 | Future updateProfilePicture(String profilePictureUrl); 28 | } 29 | 30 | abstract class BaseStorageProvider extends BaseProvider{ 31 | Future uploadFile(File file, String path); 32 | } 33 | 34 | abstract class BaseChatProvider extends BaseProvider{ 35 | Stream> getConversations(); 36 | Stream> getMessages(String chatId); 37 | Future> getPreviousMessages(String chatId, Message prevMessage); 38 | Future> getAttachments(String chatId, int type); 39 | Stream> getChats(); 40 | Future sendMessage(String chatId, Message message); 41 | Future getChatIdByUsername(String username); 42 | Future createChatIdForContact(MessioUser user); 43 | } 44 | abstract class BaseDeviceStorageProvider extends BaseProvider{ 45 | Future getThumbnail(String videoUrl); 46 | } -------------------------------------------------------------------------------- /lib/providers/storage_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:path/path.dart'; 3 | import 'package:firebase_storage/firebase_storage.dart'; 4 | import 'package:messio/providers/base_providers.dart'; 5 | 6 | class StorageProvider extends BaseStorageProvider{ 7 | final FirebaseStorage firebaseStorage; 8 | StorageProvider({FirebaseStorage firebaseStorage}): firebaseStorage = firebaseStorage ?? FirebaseStorage.instance; 9 | @override 10 | Future uploadFile(File file, String path) async{ 11 | String fileName = basename(file.path); 12 | final miliSecs = DateTime.now().millisecondsSinceEpoch; 13 | final reference = firebaseStorage.ref().child('$path/$miliSecs\_$fileName'); // get a reference to the path of the image directory 14 | String uploadPath = reference.fullPath; 15 | print('uploading to $uploadPath'); 16 | final uploadTask = reference.putFile(file); // put the file in the path 17 | final result = await uploadTask.whenComplete(()=>{}); // wait for the upload to complete 18 | String url = await result.ref.getDownloadURL(); //retrieve the download link and return it 19 | return url; 20 | } 21 | 22 | @override 23 | void dispose() {} 24 | } -------------------------------------------------------------------------------- /lib/repositories/authentication_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:messio/providers/authentication_provider.dart'; 3 | import 'package:messio/providers/base_providers.dart'; 4 | import 'package:messio/repositories/base_repository.dart'; 5 | 6 | class AuthenticationRepository extends BaseRepository { 7 | 8 | BaseAuthenticationProvider authenticationProvider = AuthenticationProvider(); 9 | 10 | Future signInWithGoogle() => 11 | authenticationProvider.signInWithGoogle(); 12 | 13 | Future signOutUser() => authenticationProvider.signOutUser(); 14 | 15 | User getCurrentUser() => authenticationProvider.getCurrentUser(); 16 | 17 | bool isLoggedIn() => authenticationProvider.isLoggedIn(); 18 | 19 | @override 20 | void dispose() { 21 | authenticationProvider.dispose(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/repositories/base_repository.dart: -------------------------------------------------------------------------------- 1 | abstract class BaseRepository{ 2 | void dispose(); 3 | } -------------------------------------------------------------------------------- /lib/repositories/chat_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:messio/models/chat.dart'; 4 | import 'package:messio/models/conversation.dart'; 5 | import 'package:messio/models/message.dart'; 6 | import 'package:messio/models/messio_user.dart'; 7 | import 'package:messio/providers/base_providers.dart'; 8 | import 'package:messio/providers/chat_provider.dart'; 9 | 10 | import 'base_repository.dart'; 11 | 12 | 13 | class ChatRepository extends BaseRepository{ 14 | BaseChatProvider chatProvider = ChatProvider(); 15 | Stream> getConversations() => chatProvider.getConversations(); 16 | Stream> getChats() => chatProvider.getChats(); 17 | Stream> getMessages(String chatId) => chatProvider.getMessages(chatId); 18 | Future> getPreviousMessages( 19 | String chatId, Message prevMessage) => 20 | chatProvider.getPreviousMessages(chatId, prevMessage); 21 | 22 | Future> getAttachments(String chatId, int type) => chatProvider.getAttachments(chatId, type); 23 | 24 | Future sendMessage(String chatId, Message message)=>chatProvider.sendMessage(chatId, message); 25 | 26 | Future getChatIdByUsername(String username) => 27 | chatProvider.getChatIdByUsername(username); 28 | 29 | Future createChatIdForContact(MessioUser user) => 30 | chatProvider.createChatIdForContact(user); 31 | 32 | @override 33 | void dispose() { 34 | chatProvider.dispose(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/repositories/storage_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:messio/providers/storage_provider.dart'; 4 | import 'package:messio/repositories/base_repository.dart'; 5 | 6 | class StorageRepository extends BaseRepository{ 7 | StorageProvider storageProvider = StorageProvider(); 8 | Future uploadFile(File file, String path) => storageProvider.uploadFile(file, path); 9 | 10 | @override 11 | void dispose() { 12 | storageProvider.dispose(); 13 | } 14 | } -------------------------------------------------------------------------------- /lib/repositories/user_data_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:messio/models/contact.dart'; 3 | import 'package:messio/models/messio_user.dart'; 4 | import 'package:messio/providers/base_providers.dart'; 5 | import 'package:messio/providers/user_data_provider.dart'; 6 | import 'package:messio/repositories/base_repository.dart'; 7 | 8 | class UserDataRepository extends BaseRepository { 9 | BaseUserDataProvider userDataProvider = UserDataProvider(); 10 | 11 | Future saveDetailsFromGoogleAuth(User user) => 12 | userDataProvider.saveDetailsFromGoogleAuth(user); 13 | 14 | Future saveProfileDetails( 15 | String uid, String profileImageUrl, int age, String username) => 16 | userDataProvider.saveProfileDetails(profileImageUrl, age, username); 17 | 18 | Future isProfileComplete() => userDataProvider.isProfileComplete(); 19 | 20 | Stream> getContacts() => userDataProvider.getContacts(); 21 | 22 | Future addContact(String username) => 23 | userDataProvider.addContact(username); 24 | 25 | Future getUser(String username) => userDataProvider.getUser(username); 26 | Future updateProfilePicture(String profilePictureUrl)=> userDataProvider.updateProfilePicture(profilePictureUrl); 27 | 28 | @override 29 | void dispose() { 30 | userDataProvider.dispose(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /lib/utils/document_snapshot_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | 3 | extension DocumentSnapshotExtension on DocumentSnapshot { 4 | get dataAsMap => this.data() as Map; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /lib/utils/exceptions.dart: -------------------------------------------------------------------------------- 1 | abstract class MessioException implements Exception{ 2 | String errorMessage(); 3 | } 4 | class UserNotFoundException extends MessioException{ 5 | @override 6 | String errorMessage() => 'No user found for provided uid/username'; 7 | 8 | } 9 | class UsernameMappingUndefinedException extends MessioException{ 10 | @override 11 | String errorMessage() =>'User not found'; 12 | 13 | } 14 | class ContactAlreadyExistsException extends MessioException{ 15 | @override 16 | String errorMessage() => 'Contact already exists!'; 17 | } -------------------------------------------------------------------------------- /lib/utils/shared_objects.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:file_picker/file_picker.dart'; 4 | import 'package:flutter_downloader/flutter_downloader.dart'; 5 | import 'package:messio/config/constants.dart'; 6 | import 'package:messio/utils/video_thumbnail.dart'; 7 | import 'package:shared_preferences/shared_preferences.dart'; 8 | 9 | class SharedObjects { 10 | static CachedSharedPreferences prefs; 11 | 12 | /* 13 | Supporting only for android for now 14 | */ 15 | static downloadFile(String fileUrl, String fileName) async { 16 | await FlutterDownloader.enqueue( 17 | url: fileUrl, 18 | fileName: fileName, 19 | savedDir: Constants.downloadsDirPath, 20 | showNotification: true, 21 | // show download progress in status bar (for Android) 22 | openFileFromNotification: true, // click on notification to open downloaded file (for Android) 23 | ); 24 | } 25 | 26 | static Future getThumbnail(String videoUrl) async { 27 | final thumbnail = await VideoThumbnail.thumbnailFile( 28 | video: videoUrl, 29 | thumbnailPath: Constants.cacheDirPath, 30 | imageFormat: ImageFormat.WEBP, 31 | maxHeightOrWidth: 0, 32 | quality: 30, 33 | ); 34 | final file = File(thumbnail); 35 | return file; 36 | } 37 | 38 | static int getTypeFromFileType(FileType fileType) { 39 | if (fileType == FileType.image) 40 | return 1; 41 | else if (fileType == FileType.video) 42 | return 2; 43 | else 44 | return 3; 45 | } 46 | } 47 | 48 | class CachedSharedPreferences { 49 | static SharedPreferences sharedPreferences; 50 | static CachedSharedPreferences instance; 51 | static final cachedKeyList = { 52 | Constants.firstRun, 53 | Constants.sessionUid, 54 | Constants.sessionUsername, 55 | Constants.sessionName, 56 | Constants.sessionProfilePictureUrl, 57 | Constants.configDarkMode, 58 | Constants.configMessagePaging, 59 | Constants.configMessagePeek, 60 | }; 61 | static final sessionKeyList = { 62 | Constants.sessionName, 63 | Constants.sessionUid, 64 | Constants.sessionUsername, 65 | Constants.sessionProfilePictureUrl 66 | }; 67 | 68 | static Map map = Map(); 69 | 70 | static Future getInstance() async { 71 | sharedPreferences = await SharedPreferences.getInstance(); 72 | if (sharedPreferences.getBool(Constants.firstRun) == null || 73 | sharedPreferences.get( 74 | Constants.firstRun)) { // if first run, then set these values 75 | await sharedPreferences.setBool(Constants.configDarkMode, false); 76 | await sharedPreferences.setBool(Constants.configMessagePaging, false); 77 | await sharedPreferences.setBool(Constants.configImageCompression, true); 78 | await sharedPreferences.setBool(Constants.configMessagePeek, true); 79 | await sharedPreferences.setBool(Constants.firstRun, false); 80 | } 81 | for (String key in cachedKeyList) { 82 | map[key] = sharedPreferences.get(key); 83 | } 84 | if (instance == null) instance = CachedSharedPreferences(); 85 | return instance; 86 | } 87 | 88 | String getString(String key) { 89 | if (cachedKeyList.contains(key)) { 90 | return map[key]; 91 | } 92 | return sharedPreferences.getString(key); 93 | } 94 | 95 | bool getBool(String key) { 96 | if (cachedKeyList.contains(key)) { 97 | return map[key]; 98 | } 99 | return sharedPreferences.getBool(key); 100 | } 101 | 102 | Future setString(String key, String value) async { 103 | bool result = await sharedPreferences.setString(key, value); 104 | if (result) 105 | map[key] = value; 106 | return result; 107 | } 108 | 109 | Future setBool(String key, bool value) async { 110 | bool result = await sharedPreferences.setBool(key, value); 111 | if (result) 112 | map[key] = value; 113 | return result; 114 | } 115 | 116 | Future clearAll() async { 117 | await sharedPreferences.clear(); 118 | map = Map(); 119 | } 120 | 121 | Future clearSession() async { 122 | await sharedPreferences.remove(Constants.sessionProfilePictureUrl); 123 | await sharedPreferences.remove(Constants.sessionUsername); 124 | await sharedPreferences.remove(Constants.sessionUid); 125 | await sharedPreferences.remove(Constants.sessionName); 126 | map.removeWhere((k, v) => (sessionKeyList.contains(k))); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /lib/utils/validators.dart: -------------------------------------------------------------------------------- 1 | class Validators { 2 | static final RegExp emailRegExp = RegExp( 3 | r'^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$', 4 | ); 5 | static final RegExp imageUrlRegExp = RegExp( 6 | r'(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|gif|png)', 7 | ); 8 | 9 | static final RegExp ageRegExp = RegExp( 10 | r'^[1-9][1-9]?$|^100$', 11 | ); 12 | 13 | static isValidEmail(String email) { 14 | return emailRegExp.hasMatch(email); 15 | } 16 | 17 | static isValidImageUrl(String imageUrl){ 18 | return imageUrlRegExp.hasMatch(imageUrl); 19 | } 20 | 21 | static isValidUsername(String username) { 22 | return true; // No solution as of now. Will implement later 23 | } 24 | 25 | static isValidAge(int age){ 26 | return ageRegExp.hasMatch(age.toString()); 27 | } 28 | } -------------------------------------------------------------------------------- /lib/utils/video_thumbnail.dart: -------------------------------------------------------------------------------- 1 | /// The Flutter plugin for creating thumbnail from video 2 | /// 3 | /// To use, import `package:video_thumbnail/video_thumbnail.dart`. 4 | /// 5 | /// See also: 6 | /// 7 | /// * [video_thumbnail](https://pub.dev/packages/video_thumbnail) 8 | /// 9 | import 'dart:async'; 10 | import 'dart:typed_data'; 11 | import 'package:flutter/foundation.dart'; 12 | import 'package:flutter/services.dart'; 13 | 14 | /// Support most popular image formats. 15 | /// Uses libwebp to encode WebP image on iOS platform. 16 | enum ImageFormat { JPEG, PNG, WEBP } 17 | 18 | class VideoThumbnail { 19 | static const MethodChannel _channel = const MethodChannel('app.messio.channel'); 20 | /// Generates a thumbnail file under specified thumbnail folder or given full path and name which matches expected ext. 21 | /// The video can be a local video file, or an URL repreents iOS or Android native supported video format. 22 | /// If the thumbnailPath is ommited or null, a thumbnail image file will be created under the same folder as the video file. 23 | /// Speicify the maximum height or width for the thumbnail or 0 for same resolution as the original video. 24 | /// The lower quality value creates lower quality of the thumbnail image, but it gets ignored for PNG format. 25 | static Future thumbnailFile( 26 | {@required String video, 27 | String thumbnailPath, 28 | ImageFormat imageFormat, 29 | int maxHeightOrWidth = 0, 30 | int quality}) async { 31 | assert(video != null && video.isNotEmpty); 32 | final reqMap = { 33 | 'video': video, 34 | 'path': thumbnailPath, 35 | 'format': imageFormat.index, 36 | 'maxhow': maxHeightOrWidth, 37 | 'quality': quality 38 | }; 39 | return await _channel.invokeMethod('file', reqMap); 40 | } 41 | 42 | /// Generates a thumbnail image data in memory as UInt8List, it can be easily used by Image.memory(...). 43 | /// The video can be a local video file, or an URL repreents iOS or Android native supported video format. 44 | /// Speicify the maximum height or width for the thumbnail or 0 for same resolution as the original video. 45 | /// The lower quality value creates lower quality of the thumbnail image, but it gets ignored for PNG format. 46 | static Future thumbnailData( 47 | {@required String video, 48 | ImageFormat imageFormat, 49 | int maxHeightOrWidth = 0, 50 | int quality}) async { 51 | assert(video != null && video.isNotEmpty); 52 | final reqMap = { 53 | 'video': video, 54 | 'format': imageFormat.index, 55 | 'maxhow': maxHeightOrWidth, 56 | 'quality': quality 57 | }; 58 | return await _channel.invokeMethod('data', reqMap); 59 | } 60 | } -------------------------------------------------------------------------------- /lib/widgets/bottom_sheet_fixed.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'dart:async'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | const Duration _kBottomSheetDuration = const Duration(milliseconds: 200); 8 | 9 | class _ModalBottomSheetLayout extends SingleChildLayoutDelegate { 10 | _ModalBottomSheetLayout(this.progress, this.bottomInset); 11 | 12 | final double progress; 13 | final double bottomInset; 14 | 15 | @override 16 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) { 17 | return BoxConstraints( 18 | minWidth: constraints.maxWidth, 19 | maxWidth: constraints.maxWidth, 20 | minHeight: 0.0, 21 | maxHeight: constraints.maxHeight 22 | ); 23 | } 24 | 25 | @override 26 | Offset getPositionForChild(Size size, Size childSize) { 27 | return Offset(0.0, size.height - bottomInset - childSize.height * progress); 28 | } 29 | 30 | @override 31 | bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) { 32 | return progress != oldDelegate.progress || bottomInset != oldDelegate.bottomInset; 33 | } 34 | } 35 | 36 | class _ModalBottomSheet extends StatefulWidget { 37 | const _ModalBottomSheet({ Key key, this.route }) : super(key: key); 38 | 39 | final _ModalBottomSheetRoute route; 40 | 41 | @override 42 | _ModalBottomSheetState createState() => _ModalBottomSheetState(); 43 | } 44 | 45 | class _ModalBottomSheetState extends State<_ModalBottomSheet> { 46 | @override 47 | Widget build(BuildContext context) { 48 | return GestureDetector( 49 | onTap: widget.route.dismissOnTap ? () => Navigator.pop(context) : null, 50 | child: AnimatedBuilder( 51 | animation: widget.route.animation, 52 | builder: (BuildContext context, Widget child) { 53 | double bottomInset = widget.route.resizeToAvoidBottomPadding 54 | ? MediaQuery.of(context).viewInsets.bottom : 0.0; 55 | return ClipRect( 56 | child: CustomSingleChildLayout( 57 | delegate: _ModalBottomSheetLayout(widget.route.animation.value, bottomInset), 58 | child: BottomSheet( 59 | animationController: widget.route._animationController, 60 | onClosing: () => Navigator.pop(context), 61 | builder: widget.route.builder 62 | ) 63 | ) 64 | ); 65 | } 66 | ) 67 | ); 68 | } 69 | } 70 | 71 | class _ModalBottomSheetRoute extends PopupRoute { 72 | _ModalBottomSheetRoute({ 73 | this.builder, 74 | this.theme, 75 | this.barrierLabel, 76 | RouteSettings settings, 77 | this.resizeToAvoidBottomPadding, 78 | this.dismissOnTap, 79 | }) : super(settings: settings); 80 | 81 | final WidgetBuilder builder; 82 | final ThemeData theme; 83 | final bool resizeToAvoidBottomPadding; 84 | final bool dismissOnTap; 85 | 86 | @override 87 | Duration get transitionDuration => _kBottomSheetDuration; 88 | 89 | @override 90 | bool get barrierDismissible => false; 91 | 92 | @override 93 | final String barrierLabel; 94 | 95 | @override 96 | Color get barrierColor => Colors.black54; 97 | 98 | AnimationController _animationController; 99 | 100 | @override 101 | AnimationController createAnimationController() { 102 | assert(_animationController == null); 103 | _animationController = BottomSheet.createAnimationController(navigator.overlay); 104 | return _animationController; 105 | } 106 | 107 | @override 108 | Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { 109 | // By definition, the bottom sheet is aligned to the bottom of the page 110 | // and isn't exposed to the top padding of the MediaQuery. 111 | Widget bottomSheet = MediaQuery.removePadding( 112 | context: context, 113 | removeTop: true, 114 | child: _ModalBottomSheet(route: this), 115 | ); 116 | if (theme != null) 117 | bottomSheet = Theme(data: theme, child: bottomSheet); 118 | return bottomSheet; 119 | } 120 | } 121 | 122 | /// Shows a modal material design bottom sheet. 123 | /// 124 | /// A modal bottom sheet is an alternative to a menu or a dialog and prevents 125 | /// the user from interacting with the rest of the app. 126 | /// 127 | /// A closely related widget is a persistent bottom sheet, which shows 128 | /// information that supplements the primary content of the app without 129 | /// preventing the use from interacting with the app. Persistent bottom sheets 130 | /// can be created and displayed with the [showBottomSheet] function or the 131 | /// [ScaffoldState.showBottomSheet] method. 132 | /// 133 | /// The `context` argument is used to look up the [Navigator] and [Theme] for 134 | /// the bottom sheet. It is only used when the method is called. Its 135 | /// corresponding widget can be safely removed from the tree before the bottom 136 | /// sheet is closed. 137 | /// 138 | /// Returns a `Future` that resolves to the value (if any) that was passed to 139 | /// [Navigator.pop] when the modal bottom sheet was closed. 140 | /// 141 | /// See also: 142 | /// 143 | /// * [BottomSheet], which is the widget normally returned by the function 144 | /// passed as the `builder` argument to [showModalBottomSheet]. 145 | /// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing 146 | /// non-modal bottom sheets. 147 | /// * 148 | Future showModalBottomSheetApp({ 149 | @required BuildContext context, 150 | @required WidgetBuilder builder, 151 | bool dismissOnTap: false, 152 | bool resizeToAvoidBottomPadding : true, 153 | }) { 154 | assert(context != null); 155 | assert(builder != null); 156 | return Navigator.push(context, _ModalBottomSheetRoute( 157 | builder: builder, 158 | theme: Theme.of(context), 159 | barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, 160 | resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, 161 | dismissOnTap: dismissOnTap, 162 | )); 163 | } -------------------------------------------------------------------------------- /lib/widgets/chat_item_widget.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:cached_network_image/cached_network_image.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:messio/config/assets.dart'; 5 | import 'package:messio/config/palette.dart'; 6 | import 'package:intl/intl.dart'; 7 | import 'package:messio/models/message.dart'; 8 | import 'package:messio/utils/shared_objects.dart'; 9 | import 'package:messio/widgets/bottom_sheet_fixed.dart'; 10 | import 'package:messio/widgets/image_full_screen_widget.dart'; 11 | import 'video_player_widget.dart'; 12 | 13 | class ChatItemWidget extends StatelessWidget { 14 | final Message message; 15 | 16 | const ChatItemWidget(this.message); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | // TODO: implement build 21 | //This is the sent message. We'll later use data from firebase instead of index to determine the message is sent or received. 22 | final isSelf = message.isSelf; 23 | return Container( 24 | child: Column(children: [ 25 | buildMessageContainer(isSelf, message, context), 26 | buildTimeStamp(context,isSelf, message) 27 | ])); 28 | } 29 | 30 | Row buildMessageContainer(bool isSelf, Message message, BuildContext context) { 31 | double lrEdgeInsets = 1.0; 32 | double tbEdgeInsets = 1.0; 33 | if (message is TextMessage) { 34 | lrEdgeInsets = 15.0; 35 | tbEdgeInsets = 10.0; 36 | } 37 | return Row( 38 | children: [ 39 | Container( 40 | child: buildMessageContent(isSelf, message,context), 41 | padding: EdgeInsets.fromLTRB( 42 | lrEdgeInsets, tbEdgeInsets, lrEdgeInsets, tbEdgeInsets), 43 | constraints: BoxConstraints(maxWidth: 200.0), 44 | decoration: BoxDecoration( 45 | color: isSelf 46 | ? Palette.selfMessageBackgroundColor 47 | : Palette.otherMessageBackgroundColor, 48 | borderRadius: BorderRadius.circular(8.0)), 49 | margin: EdgeInsets.only( 50 | right: isSelf ? 10.0 : 0, left: isSelf ? 0 : 10.0), 51 | ) 52 | ], 53 | mainAxisAlignment: isSelf 54 | ? MainAxisAlignment.end 55 | : MainAxisAlignment.start, // aligns the chatitem to right end 56 | ); 57 | } 58 | 59 | buildMessageContent(bool isSelf, Message message, BuildContext context) { 60 | if (message is TextMessage) { 61 | return Text( 62 | message.text, 63 | style: TextStyle( 64 | color: 65 | isSelf ? Palette.selfMessageColor : Palette.otherMessageColor), 66 | ); 67 | } else if (message is ImageMessage) { 68 | return GestureDetector( 69 | onTap: ()=> Navigator.push(context, MaterialPageRoute(builder: (_) => ImageFullScreen('ImageMessage_${message.documentId}',message.imageUrl))), 70 | child: Hero( 71 | tag: 'ImageMessage_${message.documentId}', 72 | child: ClipRRect( 73 | borderRadius: BorderRadius.circular(8.0), 74 | child: CachedNetworkImage(imageUrl:message.imageUrl, placeholder: (_,url)=>Image.asset(Assets.placeholder))), 75 | ), 76 | ); 77 | } else if (message is VideoMessage) { 78 | return ClipRRect( 79 | borderRadius: BorderRadius.circular(8.0), 80 | child: Column( 81 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 82 | children: [ 83 | Stack( 84 | alignment: AlignmentDirectional.center, 85 | children: [ 86 | Container( 87 | width: 130, 88 | color: Palette.secondaryColor, 89 | height: 80, 90 | ), 91 | Column( 92 | children: [ 93 | Icon( 94 | Icons.videocam, 95 | color: Palette.primaryColor, 96 | ), 97 | SizedBox( 98 | height: 5, 99 | ), 100 | Text( 101 | 'Video', 102 | style: TextStyle( 103 | fontSize: 20, 104 | color: isSelf 105 | ? Palette.selfMessageColor 106 | : Palette.otherMessageColor), 107 | ), 108 | ], 109 | ), 110 | ], 111 | ), 112 | Container( 113 | height: 40, 114 | child: IconButton( 115 | icon: Icon( 116 | Icons.play_arrow, 117 | color: isSelf 118 | ? Palette.selfMessageColor 119 | : Palette.otherMessageColor, 120 | ), 121 | onPressed: () =>showVideoPlayer(context,message.videoUrl))) 122 | ], 123 | ), 124 | ); 125 | }else if(message is FileMessage){ 126 | return ClipRRect( 127 | borderRadius: BorderRadius.circular(8.0), 128 | child: Column( 129 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 130 | children: [ 131 | Stack( 132 | alignment: AlignmentDirectional.center, 133 | children: [ 134 | Container( 135 | color: Palette.secondaryColor, 136 | height: 80, 137 | ), 138 | Column( 139 | children: [ 140 | Icon( 141 | Icons.insert_drive_file, 142 | color: Palette.primaryColor, 143 | ), 144 | SizedBox( 145 | height: 5, 146 | ), 147 | Text( 148 | message.fileName, 149 | style: TextStyle( 150 | fontSize: 14, 151 | color: isSelf 152 | ? Palette.selfMessageColor 153 | : Palette.otherMessageColor), 154 | ), 155 | ], 156 | ), 157 | ], 158 | ), 159 | Container( 160 | height: 40, 161 | child: IconButton( 162 | icon: Icon( 163 | Icons.file_download, 164 | color: isSelf 165 | ? Palette.selfMessageColor 166 | : Palette.otherMessageColor, 167 | ), 168 | onPressed: () => SharedObjects.downloadFile(message.fileUrl,message.fileName))) 169 | ], 170 | ), 171 | ); 172 | } 173 | } 174 | 175 | Row buildTimeStamp(BuildContext context, bool isSelf, Message message) { 176 | return Row( 177 | mainAxisAlignment: 178 | isSelf ? MainAxisAlignment.end : MainAxisAlignment.start, 179 | children: [ 180 | Container( 181 | child: Text( 182 | DateFormat('dd MMM kk:mm').format( 183 | DateTime.fromMillisecondsSinceEpoch(message.timeStamp)), 184 | style: Theme.of(context).textTheme.caption, 185 | ), 186 | margin: EdgeInsets.only( 187 | left: isSelf ? 5.0 : 0.0, 188 | right: isSelf ? 0.0 : 5.0, 189 | top: 5.0, 190 | bottom: 5.0), 191 | ) 192 | ]); 193 | } 194 | 195 | void showVideoPlayer(parentContext,String videoUrl) async { 196 | await showModalBottomSheetApp( 197 | context: parentContext, 198 | builder: (BuildContext bc) { 199 | return VideoPlayerWidget(videoUrl); 200 | }); 201 | } 202 | 203 | 204 | 205 | 206 | } 207 | -------------------------------------------------------------------------------- /lib/widgets/chat_list_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:messio/blocs/chats/bloc.dart'; 4 | import 'package:messio/models/chat.dart'; 5 | import 'package:messio/models/message.dart'; 6 | 7 | import 'chat_item_widget.dart'; 8 | 9 | class ChatListWidget extends StatefulWidget { 10 | final Chat chat; 11 | 12 | ChatListWidget(this.chat); 13 | 14 | @override 15 | _ChatListWidgetState createState() => _ChatListWidgetState(chat); 16 | } 17 | 18 | class _ChatListWidgetState extends State { 19 | final ScrollController listScrollController = ScrollController(); 20 | List messages = List(); 21 | final Chat chat; 22 | 23 | _ChatListWidgetState(this.chat); 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | listScrollController.addListener(() { 29 | double maxScroll = listScrollController.position.maxScrollExtent; 30 | double currentScroll = listScrollController.position.pixels; 31 | if (maxScroll == currentScroll) { 32 | BlocProvider.of(context) 33 | .add(FetchPreviousMessagesEvent(this.chat,messages.last)); 34 | } 35 | }); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | // TODO: implement build 41 | return BlocBuilder(builder: (context, state) { 42 | print(state); 43 | if (state is FetchedMessagesState) { 44 | print('Received Messages'); 45 | if (state.username == chat.username) { 46 | print(state.messages.length); 47 | print(state.isPrevious); 48 | if (state.isPrevious) 49 | messages.addAll(state.messages); 50 | else 51 | messages = state.messages; 52 | } 53 | } 54 | return ListView.builder( 55 | padding: EdgeInsets.all(10.0), 56 | itemBuilder: (context, index) => ChatItemWidget(messages[index]), 57 | itemCount: messages.length, 58 | reverse: true, 59 | controller: listScrollController, 60 | ); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/widgets/circle_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:messio/config/palette.dart'; 3 | 4 | // ignore: must_be_immutable 5 | class CircleIndicator extends StatefulWidget{ 6 | bool isActive; 7 | CircleIndicator(this.isActive); 8 | 9 | @override 10 | _CircleIndicatorState createState() => _CircleIndicatorState(); 11 | } 12 | 13 | class _CircleIndicatorState extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | return AnimatedContainer( 17 | duration: Duration(milliseconds: 600), 18 | margin: EdgeInsets.symmetric(horizontal: 8), 19 | height: widget.isActive ? 12 : 8, 20 | width: widget.isActive ? 12 : 8, 21 | decoration: BoxDecoration( 22 | color: 23 | widget.isActive ? Palette.primaryColor : Palette.secondaryTextColorLight, 24 | borderRadius: BorderRadius.all(Radius.circular(12))), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/widgets/contact_row_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:messio/blocs/config/bloc.dart'; 4 | import 'package:messio/config/constants.dart'; 5 | import 'package:messio/config/transitions.dart'; 6 | import 'package:messio/models/contact.dart'; 7 | import 'package:messio/pages/conversation_page_slide.dart'; 8 | import 'package:messio/pages/single_conversation_page.dart'; 9 | import 'package:messio/utils/shared_objects.dart'; 10 | 11 | // ignore: must_be_immutable 12 | class ContactRowWidget extends StatelessWidget { 13 | ContactRowWidget({ 14 | Key key, 15 | @required this.contact, 16 | }) : super(key: key); 17 | final Contact contact; 18 | bool configMessagePaging = false; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return BlocBuilder( 23 | builder: (context, state) { 24 | if (state is UnConfigState) 25 | configMessagePaging = 26 | SharedObjects.prefs.getBool(Constants.configMessagePaging); 27 | if (state is ConfigChangeState) if (state.key == 28 | Constants.configMessagePaging) configMessagePaging = state.value; 29 | 30 | return InkWell( 31 | onTap: () => 32 | Navigator.push(context,SlideLeftRoute(page: configMessagePaging 33 | ? ConversationPageSlide( 34 | startContact:contact) 35 | : SingleConversationPage( 36 | contact: contact, 37 | ))), 38 | child: Container( 39 | color: Theme.of(context).primaryColor, 40 | child: Padding( 41 | padding: const EdgeInsets.only(left: 30, top: 10, bottom: 10), 42 | child: RichText( 43 | text: TextSpan( 44 | style: Theme.of(context).textTheme.bodyText1, 45 | children: [ 46 | TextSpan(text: contact.getFirstName()), 47 | TextSpan( 48 | text: ' ' + contact.getLastName(), 49 | style: TextStyle(fontWeight: FontWeight.bold)), 50 | ], 51 | ), 52 | ))), 53 | ); 54 | } 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/widgets/conversation_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:messio/widgets/conversation_list_widget.dart'; 3 | 4 | import 'package:messio/widgets/navigation_pill_widget.dart'; 5 | 6 | class ConversationBottomSheet extends StatefulWidget { 7 | @override 8 | _ConversationBottomSheetState createState() => 9 | _ConversationBottomSheetState(); 10 | 11 | const ConversationBottomSheet(); 12 | } 13 | 14 | class _ConversationBottomSheetState extends State { 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return SafeArea( 19 | child: Scaffold( 20 | backgroundColor: Theme.of(context).primaryColor, 21 | body: ListView(children: [ 22 | GestureDetector( 23 | child: ListView( 24 | shrinkWrap: true, 25 | physics: ClampingScrollPhysics(), 26 | children: [ 27 | NavigationPillWidget(), 28 | Center( 29 | child: Text('Messages', style: Theme.of(context).textTheme.headline6)), 30 | SizedBox( 31 | height: 20, 32 | ), 33 | ]), 34 | onVerticalDragEnd: (details) { 35 | if (details.primaryVelocity > 50) { 36 | Navigator.pop(context); 37 | } 38 | }, 39 | ), 40 | ConversationListWidget(), 41 | ]))); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/widgets/conversation_list_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:messio/blocs/home/bloc.dart'; 4 | import 'package:messio/models/conversation.dart'; 5 | import 'chat_row_widget.dart'; 6 | 7 | class ConversationListWidget extends StatefulWidget { 8 | @override 9 | State createState() => _ConversationListWidgetState(); 10 | } 11 | 12 | class _ConversationListWidgetState extends State { 13 | HomeBloc homeBloc; 14 | List conversations = List(); 15 | 16 | @override 17 | void initState() { 18 | homeBloc = BlocProvider.of(context); 19 | homeBloc.add(FetchHomeChatsEvent()); 20 | super.initState(); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return BlocBuilder(builder: (context, state) { 26 | if (state is FetchingHomeChatsState) { 27 | return Center(child: CircularProgressIndicator()); 28 | } else if (state is FetchedHomeChatsState) { 29 | conversations = state.conversations; 30 | } 31 | return ListView.builder( 32 | shrinkWrap: true, 33 | itemCount: conversations.length, 34 | itemBuilder: (context, index) => ChatRowWidget(conversations[index])); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/widgets/gradient_fab.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:messio/config/palette.dart'; 5 | 6 | class GradientFab extends StatelessWidget { 7 | const GradientFab({ 8 | Key key, 9 | this.animation, 10 | this.vsync, 11 | this.elevation, 12 | @required this.child, 13 | @required this.onPressed, 14 | }) : super(key: key); 15 | 16 | final Animation animation; 17 | final TickerProvider vsync; 18 | final VoidCallback onPressed; 19 | final Widget child; 20 | final double elevation; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | var fab =FloatingActionButton( 25 | elevation: elevation!=null?elevation:6, 26 | child: Container( 27 | constraints: BoxConstraints.expand(), 28 | decoration: BoxDecoration( 29 | shape: BoxShape.circle, 30 | gradient: LinearGradient( 31 | begin: Alignment.center, 32 | end: Alignment.bottomRight, 33 | colors: [ 34 | Palette.gradientStartColor, 35 | Palette.gradientEndColor 36 | ])), 37 | child: child, 38 | ), 39 | onPressed: onPressed, 40 | ); 41 | return animation!=null?AnimatedSize( 42 | duration: Duration(milliseconds: 1000), 43 | curve: Curves.linear, 44 | vsync: vsync, 45 | child: ScaleTransition( 46 | scale: animation, 47 | child: fab)):fab; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/widgets/gradient_snack_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flushbar/flushbar.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:messio/config/palette.dart'; 5 | 6 | class GradientSnackBar{ 7 | static void showMessage(BuildContext context, String message){ 8 | Flushbar( 9 | message: message, 10 | duration: Duration(milliseconds: 1500), 11 | backgroundGradient:LinearGradient( 12 | begin: Alignment.center, 13 | end: Alignment.bottomRight, 14 | colors: [ 15 | Palette.gradientStartColor, 16 | Palette.gradientEndColor 17 | ]) 18 | ,backgroundColor: Colors.red, 19 | boxShadows: [BoxShadow(color: Colors.blue[800], offset: Offset(0.0, 2.0), blurRadius: 3.0,)], 20 | )..show(context); 21 | } 22 | static void showError(BuildContext context, String error){ 23 | Flushbar( 24 | message: error, 25 | duration: Duration(milliseconds: 1500), 26 | backgroundGradient:LinearGradient( 27 | begin: Alignment.center, 28 | end: Alignment.bottomRight, 29 | colors: [ 30 | Palette.errorGradientStartColor, 31 | Palette.errorGradientEndColor 32 | ]) 33 | ,backgroundColor: Colors.red, 34 | boxShadows: [BoxShadow(color: Colors.blue[800], offset: Offset(0.0, 2.0), blurRadius: 3.0,)], 35 | )..show(context); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /lib/widgets/image_full_screen_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ImageFullScreen extends StatelessWidget { 5 | final String url; 6 | final String tag; 7 | ImageFullScreen(this.tag,this.url); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Container( 12 | color: Colors.black, 13 | child: Hero( 14 | tag: tag, 15 | child: CachedNetworkImage(imageUrl: url,)), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/widgets/input_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:emoji_picker/emoji_picker.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:messio/blocs/chats/bloc.dart'; 5 | class InputWidget extends StatefulWidget { 6 | @override 7 | _InputWidgetState createState() => _InputWidgetState(); 8 | } 9 | 10 | class _InputWidgetState extends State{ 11 | final TextEditingController textEditingController = TextEditingController(); 12 | ChatBloc chatBloc; 13 | bool showEmojiKeyboard = false; 14 | @override 15 | void initState() { 16 | chatBloc = BlocProvider.of(context); 17 | super.initState(); 18 | } 19 | @override 20 | Widget build(BuildContext context) { 21 | return Material( 22 | elevation: 60.0, 23 | child: Container( 24 | child: Column( 25 | children: [ 26 | Row( 27 | children: [ 28 | Material( 29 | child: Container( 30 | margin: EdgeInsets.symmetric(horizontal: 1.0), 31 | child: IconButton( 32 | icon: Icon(Icons.face), 33 | color: Theme.of(context).accentColor, 34 | onPressed: () =>chatBloc.add(ToggleEmojiKeyboardEvent(!showEmojiKeyboard)), 35 | ), 36 | ), 37 | color: Theme.of(context).primaryColor, 38 | ), 39 | 40 | // Text input 41 | Flexible( 42 | child: Material( 43 | child: Container( 44 | color: Theme.of(context).primaryColor, 45 | child: TextField( 46 | style: Theme.of(context).textTheme.bodyText1, 47 | controller: textEditingController, 48 | autofocus: true, 49 | decoration: InputDecoration.collapsed( 50 | hintText: 'Type a message', 51 | hintStyle: TextStyle(color: Theme.of(context).hintColor), 52 | ), 53 | ), 54 | )), 55 | ), 56 | 57 | // Send Message Button 58 | Material( 59 | child: Container( 60 | margin: EdgeInsets.symmetric(horizontal: 8.0), 61 | child: IconButton( 62 | icon: Icon(Icons.send), 63 | onPressed: () => sendMessage(context), 64 | color:Theme.of(context).accentColor, 65 | ), 66 | ), 67 | color: Theme.of(context).primaryColor, 68 | ), 69 | ], 70 | ), 71 | BlocBuilder(builder: (context, state) { 72 | showEmojiKeyboard = state is ToggleEmojiKeyboardState && 73 | state.showEmojiKeyboard; 74 | if (!showEmojiKeyboard) return Container(); 75 | //hide keyboard 76 | FocusScope.of(context).requestFocus(new FocusNode()); 77 | //create emojipicker 78 | return EmojiPicker( 79 | rows: 4, 80 | columns: 7, 81 | bgColor: Theme.of(context).backgroundColor, 82 | indicatorColor: Theme.of(context).accentColor, 83 | onEmojiSelected: (emoji, category) { 84 | textEditingController.text = textEditingController.text+ emoji.emoji; 85 | }, 86 | ); 87 | }) 88 | ], 89 | ), 90 | width: double.infinity, 91 | decoration: BoxDecoration( 92 | border: 93 | Border(top: BorderSide(color: Theme.of(context).hintColor, width: 0.5)), 94 | color: Theme.of(context).primaryColor, 95 | ), 96 | )); 97 | } 98 | 99 | void sendMessage(context) { 100 | if(textEditingController.text.isEmpty) 101 | return; 102 | chatBloc.add(SendTextMessageEvent(textEditingController.text)); 103 | textEditingController.clear(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/widgets/navigation_pill_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:messio/config/palette.dart'; 3 | 4 | class NavigationPillWidget extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | // TODO: implement build 8 | return Container( 9 | child: Column( 10 | mainAxisAlignment: MainAxisAlignment.center, 11 | crossAxisAlignment: CrossAxisAlignment.stretch, 12 | children: [ 13 | Container( 14 | child: Center( 15 | child: Wrap(children: [ 16 | Container( 17 | width: 50, 18 | margin: EdgeInsets.only(top: 10, bottom: 10), 19 | height: 5, 20 | decoration: BoxDecoration( 21 | color: Palette.accentColor, 22 | shape: BoxShape.rectangle, 23 | borderRadius: BorderRadius.all( 24 | Radius.circular(8.0)), 25 | ) 26 | ), 27 | ]))), 28 | ])); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/widgets/quick_scroll_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class QuickScrollBar extends StatefulWidget { 4 | final List nameList; 5 | final ScrollController scrollController; 6 | 7 | QuickScrollBar({@required this.nameList,@required this.scrollController}); 8 | 9 | @override 10 | _QuickScrollBarState createState() => _QuickScrollBarState(nameList,scrollController); 11 | } 12 | 13 | class _QuickScrollBarState extends State { 14 | double offsetContainer = 0.0; 15 | var scrollBarText; 16 | var scrollBarTextPrev; 17 | var scrollBarHeight; 18 | var contactRowSize = 45.0; //NOTE: size items 19 | var scrollBarMarginRight = 50.0; 20 | var scrollBarContainerHeight; 21 | var scrollBarPosSelected = 0; 22 | var scrollBarHeightDiff = 0.0; 23 | var screenHeight = 0.0; 24 | ScrollController scrollController; 25 | String scrollBarBubbleText = ""; 26 | bool scrollBarBubbleVisibility = false; 27 | List nameList; 28 | 29 | _QuickScrollBarState(this.nameList,this.scrollController); 30 | 31 | List alphabetList = [ 32 | 'A', 33 | 'B', 34 | 'C', 35 | 'D', 36 | 'E', 37 | 'F', 38 | 'G', 39 | 'H', 40 | 'I', 41 | 'J', 42 | 'K', 43 | 'L', 44 | 'M', 45 | 'N', 46 | 'O', 47 | 'P', 48 | 'Q', 49 | 'R', 50 | 'S', 51 | 'T', 52 | 'U', 53 | 'V', 54 | 'W', 55 | 'X', 56 | 'Y', 57 | 'Z' 58 | ]; 59 | 60 | void _onVerticalDragUpdate(DragUpdateDetails details) { 61 | setState(() { 62 | scrollBarBubbleVisibility = true; 63 | print(offsetContainer); 64 | print(details); 65 | if ((offsetContainer + details.delta.dy) >= 0 && 66 | (offsetContainer + details.delta.dy) <= 67 | (scrollBarContainerHeight - scrollBarHeight)) { 68 | offsetContainer += details.delta.dy; 69 | print(offsetContainer); 70 | scrollBarPosSelected = 71 | ((offsetContainer / scrollBarHeight) % alphabetList.length).round(); 72 | print(scrollBarPosSelected); 73 | scrollBarText = alphabetList[scrollBarPosSelected]; 74 | if (scrollBarText != scrollBarTextPrev) { 75 | for (var i = 0; i < nameList.length; i++) { 76 | print(scrollBarText.toString()); 77 | if (scrollBarText 78 | .toString() 79 | .compareTo(nameList[i].toString().toUpperCase()[0]) == 80 | 0) { 81 | print(nameList[i]); 82 | scrollController.jumpTo(i * contactRowSize); 83 | break; 84 | } 85 | } 86 | scrollBarTextPrev = scrollBarText; 87 | } 88 | } 89 | }); 90 | } 91 | 92 | void _onVerticalDragStart(DragStartDetails details) { 93 | offsetContainer = details.globalPosition.dy - scrollBarHeightDiff; 94 | setState(() { 95 | scrollBarBubbleVisibility = true; 96 | }); 97 | } 98 | 99 | getBubble() { 100 | if (!scrollBarBubbleVisibility) { 101 | return Container(); 102 | } 103 | return Container( 104 | decoration: BoxDecoration( 105 | color: Theme.of(context).accentColor, 106 | borderRadius: BorderRadius.all(const Radius.circular(30.0))), 107 | width: 30, 108 | height: 30, 109 | child: Center( 110 | child: Text( 111 | "${scrollBarText ?? "${alphabetList.first}"}", 112 | style:Theme.of(context).textTheme.bodyText2, 113 | ), 114 | ), 115 | ); 116 | } 117 | 118 | 119 | _getAlphabetItem(int index) { 120 | return Expanded( 121 | child: Container( 122 | width: 40, 123 | height: 20, 124 | alignment: Alignment.center, 125 | child: Text( 126 | alphabetList[index], 127 | style: (index == scrollBarPosSelected) 128 | ? TextStyle(fontSize: 16, fontWeight: FontWeight.w700) 129 | : TextStyle(fontSize: 12, fontWeight: FontWeight.w400), 130 | ), 131 | ), 132 | ); 133 | } 134 | 135 | void _onVerticalEnd(DragEndDetails details) { 136 | setState(() { 137 | scrollBarBubbleVisibility = false; 138 | }); 139 | } 140 | 141 | @override 142 | Widget build(BuildContext context) { 143 | screenHeight = MediaQuery.of(context).size.height; 144 | return LayoutBuilder(builder: (context, constraints) { 145 | scrollBarHeightDiff = screenHeight - constraints.biggest.height; 146 | scrollBarHeight = (constraints.biggest.height) / alphabetList.length; 147 | scrollBarContainerHeight = (constraints.biggest.height); //NO 148 | return Stack( 149 | children: [ 150 | Align( 151 | alignment: Alignment.centerRight, 152 | child: GestureDetector( 153 | onVerticalDragEnd: _onVerticalEnd, 154 | onVerticalDragUpdate: _onVerticalDragUpdate, 155 | onVerticalDragStart: _onVerticalDragStart, 156 | child: Container( 157 | //height: 20.0 * 26, 158 | color: Colors.transparent, 159 | child: Column( 160 | mainAxisAlignment: MainAxisAlignment.center, 161 | children: []..addAll( 162 | List.generate( 163 | alphabetList.length, (index) => _getAlphabetItem(index)), 164 | ), 165 | ), 166 | ), 167 | ), 168 | ), 169 | Positioned( 170 | right: scrollBarMarginRight, 171 | top: offsetContainer, 172 | child: getBubble(), 173 | ), 174 | ], 175 | ); 176 | }); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/widgets/video_player_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:video_player/video_player.dart'; 3 | 4 | import 'gradient_fab.dart'; 5 | 6 | class VideoPlayerWidget extends StatefulWidget { 7 | final String videoUrl; 8 | 9 | VideoPlayerWidget(this.videoUrl); 10 | 11 | @override 12 | _VideoPlayerWidgetState createState() => _VideoPlayerWidgetState(videoUrl); 13 | } 14 | 15 | class _VideoPlayerWidgetState extends State { 16 | final VideoPlayerController videoPlayerController; 17 | final String videoUrl; 18 | double videoDuration = 0; 19 | double currentDuration = 0; 20 | _VideoPlayerWidgetState(this.videoUrl) 21 | : videoPlayerController = VideoPlayerController.network(videoUrl); 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | videoPlayerController.initialize().then((_) { 27 | setState(() { 28 | videoDuration = 29 | videoPlayerController.value.duration.inMilliseconds.toDouble(); 30 | }); 31 | 32 | }); 33 | 34 | videoPlayerController.addListener(() { 35 | setState(() { 36 | currentDuration = videoPlayerController.value.position.inMilliseconds.toDouble(); 37 | }); 38 | }); 39 | print(videoUrl); 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return Container( 45 | color: Color(0xFF737373), 46 | // This line set the transparent background 47 | child: Container( 48 | color: Theme.of(context).backgroundColor, 49 | child: Column( 50 | mainAxisSize: MainAxisSize.min, 51 | children: [ 52 | Container( 53 | color: Theme.of(context).primaryColor, 54 | constraints: BoxConstraints(maxHeight: 400), 55 | child: videoPlayerController.value.isInitialized 56 | ? AspectRatio( 57 | aspectRatio: videoPlayerController.value.aspectRatio, 58 | child: VideoPlayer(videoPlayerController), 59 | ) 60 | : Container( 61 | height: 200, 62 | color: Theme.of(context).primaryColor, 63 | ), 64 | ), 65 | Slider( 66 | value: currentDuration, 67 | max: videoDuration, 68 | onChanged: (value) => videoPlayerController 69 | .seekTo(Duration(milliseconds: value.toInt())), 70 | ), 71 | Padding( 72 | padding: const EdgeInsets.only(bottom:24.0), 73 | child: GradientFab( 74 | elevation: 0, 75 | child: Icon( 76 | videoPlayerController.value.isPlaying 77 | ? Icons.pause 78 | : Icons.play_arrow, 79 | color: Theme.of(context).primaryColor, 80 | ), 81 | onPressed: () { 82 | setState(() { 83 | if (videoPlayerController.value.buffered.length != 0 && 84 | videoPlayerController.value.position == 85 | videoPlayerController.value.buffered[0].end) { 86 | videoPlayerController.seekTo(Duration(seconds: 0)); 87 | } 88 | videoPlayerController.value.isPlaying 89 | ? videoPlayerController.pause() 90 | : videoPlayerController.play(); 91 | }); 92 | }), 93 | ) 94 | ], 95 | )), 96 | ); 97 | } 98 | 99 | @override 100 | void dispose() { 101 | videoPlayerController.dispose(); 102 | super.dispose(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: messio 2 | description: Messio is a modern messaging app 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name iabs used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.6.0 <3.0.0" 18 | flutter: 3.0.3 19 | 20 | dependencies: 21 | flutter: 22 | sdk: flutter 23 | intl: ^0.17.0 24 | cupertino_icons: ^1.0.5 25 | infinite_listview: ^1.1.0 26 | firebase_core: ^2.3.0 27 | firebase_auth: ^4.1.5 #to auth with firebase 28 | google_sign_in: ^5.4.2 #to auth with google account 29 | flutter_bloc: ^8.1.1 #heedelps in implementing blocs 30 | equatable: ^2.0.5 #helps in comparing class using their values 31 | cloud_firestore: ^4.1.0 #Firebase Database 32 | image_picker: ^0.7.2+1 #image picker used for picking images from gallery 33 | firebase_storage: ^11.0.6 #storage for storing image files 34 | flutter_launcher_icons: ^0.11.0 35 | shared_preferences: ^0.5.5 36 | file_picker: ^5.2.3 37 | emoji_picker: ^0.1.0 38 | video_player: ^2.4.8 39 | flutter_downloader: ^1.9.1 40 | downloads_path_provider: ^0.1.0 41 | flushbar: ^1.10.4 42 | path_provider: ^2.0.11 43 | cached_network_image: ^3.2.1 44 | 45 | dev_dependencies: 46 | flutter_test: 47 | sdk: flutter 48 | mockito: ^5.3.2 49 | 50 | flutter_icons: 51 | ios: true 52 | android: true 53 | image_path_ios: "assets/launcher/ic_launcher.png" 54 | image_path_android: "assets/launcher/ic_launcher.png" 55 | adaptive_icon_background: "assets/launcher/ic_background.png" 56 | adaptive_icon_foreground: "assets/launcher/ic_foreground.png" 57 | 58 | # For information on the generic Dart part of this file, see the 59 | # following page: https://dart.dev/tools/pub/pubspec 60 | 61 | # The following section is specific to Flutter. 62 | flutter: 63 | assets: 64 | - assets/ 65 | - assets/launcher/ 66 | - assets/fonts/ 67 | # The following line ensures that the Material Icons font is 68 | # included with your application, so that you can use the icons in 69 | # the material Icons class. 70 | uses-material-design: true 71 | fonts: 72 | - family: Manrope 73 | fonts: 74 | - asset: assets/fonts/manrope-regular.otf 75 | - asset: assets/fonts/manrope-bold.otf 76 | weight: 700 77 | - asset: assets/fonts/manrope-extrabold.otf 78 | weight: 800 79 | - asset: assets/fonts/manrope-semibold.otf 80 | weight: 400 81 | # - family: Trajan Pro 82 | # fonts: 83 | # - asset: fonts/TrajanPro.ttf 84 | # - asset: fonts/TrajanPro_Bold.ttf 85 | # weight: 700 86 | # 87 | # For details regarding fonts from package dependencies, 88 | # see https://flutter.dev/custom-fonts/#from-packages 89 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase/ 11 | 12 | # Firebase config 13 | 14 | # Uncomment this if you'd like others to create their own Firebase project. 15 | # For a team working on the same Firebase project(s), it is recommended to leave 16 | # it commented so all members can deploy to the same project(s) in .firebaserc. 17 | # .firebaserc 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | -------------------------------------------------------------------------------- /server/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "functions": { 7 | "predeploy": [ 8 | "npm --prefix \"$RESOURCE_DIR\" run lint", 9 | "npm --prefix \"$RESOURCE_DIR\" run build" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [ 3 | { 4 | "collectionGroup": "chats", 5 | "queryScope": "COLLECTION", 6 | "fields": [ 7 | { 8 | "fieldPath": "members", 9 | "arrayConfig": "CONTAINS" 10 | }, 11 | { 12 | "fieldPath": "latestMessage.timeStamp", 13 | "order": "ASCENDING" 14 | } 15 | ] 16 | }, 17 | { 18 | "collectionGroup": "chats", 19 | "queryScope": "COLLECTION", 20 | "fields": [ 21 | { 22 | "fieldPath": "members", 23 | "arrayConfig": "CONTAINS" 24 | }, 25 | { 26 | "fieldPath": "latestMessage.timeStamp", 27 | "order": "DESCENDING" 28 | } 29 | ] 30 | }, 31 | { 32 | "collectionGroup": "messages", 33 | "queryScope": "COLLECTION", 34 | "fields": [ 35 | { 36 | "fieldPath": "type", 37 | "order": "ASCENDING" 38 | }, 39 | { 40 | "fieldPath": "timeStamp", 41 | "order": "DESCENDING" 42 | } 43 | ] 44 | } 45 | ], 46 | "fieldOverrides": [] 47 | } 48 | -------------------------------------------------------------------------------- /server/firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /users/{document=**} { 5 | 6 | //Allow creating a new user to anyone who is authenticated 7 | allow create: if isSignedIn(); 8 | 9 | //Allow read if signed in 10 | allow read: if isSignedIn(); 11 | 12 | //Allow write if isSignedIn() cannot check for uid matching here because other users can also edit to add new contact. Will add validation to it later 13 | allow write: if isSignedIn(); 14 | 15 | // Allow update only if the uid matches (same user) 16 | allow update: if isSignedIn() && request.auth.uid == resource.data.uid; 17 | 18 | // Allow delete only if the uid matches (same user) 19 | allow delete: if isSignedIn() && request.auth.uid == resource.data.uid; 20 | 21 | } 22 | 23 | match /username_uid_map/{document=**} { 24 | 25 | allow create, read : if isSignedIn(); //Once a uid mapping is created it cannot be deleted or updated from the app 26 | 27 | } 28 | 29 | match /chats/{document=**} { 30 | //Allow users to only create and read chats. Delete and update not available right now 31 | allow create,read : if isSignedIn(); 32 | 33 | } 34 | } 35 | } 36 | 37 | function isSignedIn() { 38 | return request.auth.uid != null; 39 | } -------------------------------------------------------------------------------- /server/functions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Compiled JavaScript files 2 | **/*.js 3 | **/*.js.map 4 | 5 | # Typescript v1 declaration files 6 | typings/ 7 | 8 | node_modules/ -------------------------------------------------------------------------------- /server/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "tslint --project tsconfig.json", 5 | "build": "tsc", 6 | "serve": "npm run build && firebase serve --only functions", 7 | "shell": "npm run build && firebase functions:shell", 8 | "start": "npm run shell", 9 | "deploy": "firebase deploy --only functions", 10 | "logs": "firebase functions:log" 11 | }, 12 | "engines": { 13 | "node": "8" 14 | }, 15 | "main": "lib/index.js", 16 | "dependencies": { 17 | "firebase-admin": "^11.4.1", 18 | "firebase-functions": "^3.24.1" 19 | }, 20 | "devDependencies": { 21 | "tslint": "^5.12.0", 22 | "typescript": "^3.2.2" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /server/functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as functions from 'firebase-functions'; 2 | 3 | // // Start writing Firebase Functions 4 | // // https://firebase.google.com/docs/functions/typescript 5 | // 6 | // export const helloWorld = functions.https.onRequest((request, response) => { 7 | // response.send("Hello from Firebase!"); 8 | // }); 9 | -------------------------------------------------------------------------------- /server/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2017" 10 | }, 11 | "compileOnSave": true, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /server/functions/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // -- Strict errors -- 4 | // These lint rules are likely always a good idea. 5 | 6 | // Force function overloads to be declared together. This ensures readers understand APIs. 7 | "adjacent-overload-signatures": true, 8 | 9 | // Do not allow the subtle/obscure comma operator. 10 | "ban-comma-operator": true, 11 | 12 | // Do not allow internal modules or namespaces . These are deprecated in favor of ES6 modules. 13 | "no-namespace": true, 14 | 15 | // Do not allow parameters to be reassigned. To avoid bugs, developers should instead assign new values to new vars. 16 | "no-parameter-reassignment": true, 17 | 18 | // Force the use of ES6-style imports instead of /// imports. 19 | "no-reference": true, 20 | 21 | // Do not allow type assertions that do nothing. This is a big warning that the developer may not understand the 22 | // code currently being edited (they may be incorrectly handling a different type case that does not exist). 23 | "no-unnecessary-type-assertion": true, 24 | 25 | // Disallow nonsensical label usage. 26 | "label-position": true, 27 | 28 | // Disallows the (often typo) syntax if (var1 = var2). Replace with if (var2) { var1 = var2 }. 29 | "no-conditional-assignment": true, 30 | 31 | // Disallows constructors for primitive types (e.g. new Number('123'), though Number('123') is still allowed). 32 | "no-construct": true, 33 | 34 | // Do not allow super() to be called twice in a constructor. 35 | "no-duplicate-super": true, 36 | 37 | // Do not allow the same case to appear more than once in a switch block. 38 | "no-duplicate-switch-case": true, 39 | 40 | // Do not allow a variable to be declared more than once in the same block. Consider function parameters in this 41 | // rule. 42 | "no-duplicate-variable": [true, "check-parameters"], 43 | 44 | // Disallows a variable definition in an inner scope from shadowing a variable in an outer scope. Developers should 45 | // instead use a separate variable name. 46 | "no-shadowed-variable": true, 47 | 48 | // Empty blocks are almost never needed. Allow the one general exception: empty catch blocks. 49 | "no-empty": [true, "allow-empty-catch"], 50 | 51 | // Functions must either be handled directly (e.g. with a catch() handler) or returned to another function. 52 | // This is a major source of errors in Cloud Functions and the team strongly recommends leaving this rule on. 53 | "no-floating-promises": true, 54 | 55 | // Do not allow any imports for modules that are not in package.json. These will almost certainly fail when 56 | // deployed. 57 | "no-implicit-dependencies": true, 58 | 59 | // The 'this' keyword can only be used inside of classes. 60 | "no-invalid-this": true, 61 | 62 | // Do not allow strings to be thrown because they will not include stack traces. Throw Errors instead. 63 | "no-string-throw": true, 64 | 65 | // Disallow control flow statements, such as return, continue, break, and throw in finally blocks. 66 | "no-unsafe-finally": true, 67 | 68 | // Expressions must always return a value. Avoids common errors like const myValue = functionReturningVoid(); 69 | "no-void-expression": [true, "ignore-arrow-function-shorthand"], 70 | 71 | // Disallow duplicate imports in the same file. 72 | "no-duplicate-imports": true, 73 | 74 | 75 | // -- Strong Warnings -- 76 | // These rules should almost never be needed, but may be included due to legacy code. 77 | // They are left as a warning to avoid frustration with blocked deploys when the developer 78 | // understand the warning and wants to deploy anyway. 79 | 80 | // Warn when an empty interface is defined. These are generally not useful. 81 | "no-empty-interface": {"severity": "warning"}, 82 | 83 | // Warn when an import will have side effects. 84 | "no-import-side-effect": {"severity": "warning"}, 85 | 86 | // Warn when variables are defined with var. Var has subtle meaning that can lead to bugs. Strongly prefer const for 87 | // most values and let for values that will change. 88 | "no-var-keyword": {"severity": "warning"}, 89 | 90 | // Prefer === and !== over == and !=. The latter operators support overloads that are often accidental. 91 | "triple-equals": {"severity": "warning"}, 92 | 93 | // Warn when using deprecated APIs. 94 | "deprecation": {"severity": "warning"}, 95 | 96 | // -- Light Warnings -- 97 | // These rules are intended to help developers use better style. Simpler code has fewer bugs. These would be "info" 98 | // if TSLint supported such a level. 99 | 100 | // prefer for( ... of ... ) to an index loop when the index is only used to fetch an object from an array. 101 | // (Even better: check out utils like .map if transforming an array!) 102 | "prefer-for-of": {"severity": "warning"}, 103 | 104 | // Warns if function overloads could be unified into a single function with optional or rest parameters. 105 | "unified-signatures": {"severity": "warning"}, 106 | 107 | // Prefer const for values that will not change. This better documents code. 108 | "prefer-const": {"severity": "warning"}, 109 | 110 | // Multi-line object literals and function calls should have a trailing comma. This helps avoid merge conflicts. 111 | "trailing-comma": {"severity": "warning"} 112 | }, 113 | 114 | "defaultSeverity": "error" 115 | } 116 | -------------------------------------------------------------------------------- /test/blocs/authentication_bloc_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:messio/blocs/authentication/authentication_bloc.dart'; 3 | import 'package:messio/blocs/authentication/authentication_event.dart'; 4 | import 'package:messio/blocs/authentication/authentication_state.dart'; 5 | import 'package:messio/models/messio_user.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | 8 | import '../mock/firebase_mock.dart'; 9 | import '../mock/io_mock.dart'; 10 | import '../mock/repository_mock.dart'; 11 | 12 | void main() { 13 | AuthenticationBloc authenticationBloc; 14 | AuthenticationRepositoryMock authenticationRepository; 15 | UserDataRepositoryMock userDataRepository; 16 | StorageRepositoryMock storageRepository; 17 | FirebaseUserMock firebaseUser; 18 | MessioUser user; 19 | MockFile file; 20 | int age; 21 | String username; 22 | String profilePictureUrl; 23 | 24 | setUp(() { 25 | userDataRepository = UserDataRepositoryMock(); 26 | authenticationRepository = AuthenticationRepositoryMock(); 27 | storageRepository = StorageRepositoryMock(); 28 | firebaseUser = FirebaseUserMock(); 29 | user = MessioUser(); 30 | file = MockFile(); 31 | age = 23; 32 | username = 'johndoe'; 33 | profilePictureUrl = 'http://www.github.com/adityadroid'; 34 | authenticationBloc = AuthenticationBloc( 35 | userDataRepository: userDataRepository, 36 | authenticationRepository: authenticationRepository, 37 | storageRepository: storageRepository); 38 | }); 39 | 40 | test('initial state is always AuthInProgress', () { 41 | expect(authenticationBloc.initialState, Uninitialized()); 42 | }); 43 | 44 | //test the sequence of event emissions for different conditions 45 | group('AppLaunched', () { 46 | test('emits [Uninitialized -> Unauthenticated] when not logged in', () { 47 | when(authenticationRepository.isLoggedIn()) 48 | .thenAnswer((_) => Future.value(false)); 49 | final expectedStates = [ 50 | Uninitialized(), 51 | AuthInProgress(), 52 | UnAuthenticated() 53 | ]; 54 | expectLater(authenticationBloc.state, emitsInOrder(expectedStates)); 55 | 56 | authenticationBloc.add(AppLaunched()); 57 | }); 58 | test('emits [Uninitialized -> ProfileUpdated] when user is logged in and profile is complete', () { 59 | when(authenticationRepository.isLoggedIn()) 60 | .thenAnswer((_) => Future.value(true)); 61 | when(authenticationRepository.getCurrentUser()) 62 | .thenAnswer((_) => Future.value(FirebaseUserMock())); 63 | when(userDataRepository.isProfileComplete()) 64 | .thenAnswer((_) => Future.value(true)); 65 | final expectedStates = [ 66 | Uninitialized(), 67 | AuthInProgress(), 68 | ProfileUpdated() 69 | ]; 70 | expectLater(authenticationBloc.state, emitsInOrder(expectedStates)); 71 | 72 | authenticationBloc.dispatch(AppLaunched()); 73 | }); 74 | test('emits [Uninitialized -> AuthInProgress -> Authenticated -> ProfileUpdateInProgress -> PreFillData] when user is logged in and profile is not complete', () { 75 | when(authenticationRepository.isLoggedIn()) 76 | .thenAnswer((_) => Future.value(true)); 77 | when(authenticationRepository.getCurrentUser()) 78 | .thenAnswer((_) => Future.value(firebaseUser)); 79 | when(userDataRepository.isProfileComplete()) 80 | .thenAnswer((_) => Future.value(false)); 81 | final expectedStates = [ 82 | Uninitialized(), 83 | AuthInProgress(), 84 | Authenticated(firebaseUser), 85 | ProfileUpdateInProgress(), 86 | PreFillData(user) 87 | ]; 88 | expectLater(authenticationBloc.state, emitsInOrder(expectedStates)); 89 | 90 | authenticationBloc.dispatch(AppLaunched()); 91 | }); 92 | }); 93 | 94 | group('ClickedGoogleLogin', () { 95 | test('emits [AuthInProgress -> ProfileUpdated] when the user clicks Google Login button and after login result, the profile is complete', () { 96 | when(authenticationRepository.signInWithGoogle()) 97 | .thenAnswer((_) => Future.value(firebaseUser)); 98 | when(userDataRepository.isProfileComplete()) 99 | .thenAnswer((_) => Future.value(true)); 100 | final expectedStates = [ 101 | Uninitialized(), 102 | AuthInProgress(), 103 | ProfileUpdated() 104 | ]; 105 | expectLater(authenticationBloc.state, emitsInOrder(expectedStates)); 106 | authenticationBloc.dispatch(ClickedGoogleLogin()); 107 | }); 108 | 109 | test('emits [AuthInProgress -> Authenticated -> ProfileUpdateInProgress -> PreFillData] when the user clicks Google Login button and after login result, the profile is found to be incomplete', () { 110 | when(authenticationRepository.signInWithGoogle()) 111 | .thenAnswer((_) => Future.value(firebaseUser)); 112 | when(userDataRepository.isProfileComplete()) 113 | .thenAnswer((_) => Future.value(false)); 114 | final expectedStates = [ 115 | Uninitialized(), 116 | AuthInProgress(), 117 | Authenticated(firebaseUser), 118 | ProfileUpdateInProgress(), 119 | PreFillData(user) 120 | ]; 121 | expectLater(authenticationBloc.state, emitsInOrder(expectedStates)); 122 | authenticationBloc.dispatch(ClickedGoogleLogin()); 123 | }); 124 | }); 125 | 126 | group('LoggedIn', () { 127 | test('emits [ProfileUpdateInProgress -> PreFillData] when trigged, this event is trigged once gauth is done and profile is not complete', () { 128 | when(userDataRepository.saveDetailsFromGoogleAuth(firebaseUser)) 129 | .thenAnswer((_) => Future.value(user)); 130 | final expectedStates = [ 131 | Uninitialized(), 132 | ProfileUpdateInProgress(), 133 | PreFillData(user) 134 | ]; 135 | expectLater(authenticationBloc.state, emitsInOrder(expectedStates)); 136 | 137 | authenticationBloc.dispatch(LoggedIn(firebaseUser)); 138 | }); 139 | }); 140 | 141 | group('PickedProfilePicture', () { 142 | test('emits [ReceivedProfilePicture] everytime', () { 143 | final expectedStates = [Uninitialized(), ReceivedProfilePicture(file)]; 144 | expectLater(authenticationBloc.state, emitsInOrder(expectedStates)); 145 | authenticationBloc.dispatch(PickedProfilePicture(file)); 146 | }); 147 | }); 148 | 149 | group('SaveProfile', () { 150 | test('emits [ProfileUpdateInProgress -> ProfileUpdated] everytime SaveProfile is dispatched', () { 151 | when(storageRepository.uploadFile(any, any)) 152 | .thenAnswer((_) => Future.value(profilePictureUrl)); 153 | when(authenticationRepository.getCurrentUser()) 154 | .thenAnswer((_) => Future.value(firebaseUser)); 155 | when(userDataRepository.saveProfileDetails(any, any, any, any)) 156 | .thenAnswer((_) => Future.value(user)); 157 | final expectedStates = [ 158 | Uninitialized(), 159 | ProfileUpdateInProgress(), 160 | ProfileUpdated() 161 | ]; 162 | expectLater(authenticationBloc.state, emitsInOrder(expectedStates)); 163 | authenticationBloc.dispatch(SaveProfile(file, age, username)); 164 | }); 165 | }); 166 | 167 | group('ClickedLogout', () { 168 | test('emits [UnAuthenticated] when clicked logout', () { 169 | final expectedStates = [Uninitialized(), UnAuthenticated()]; 170 | expectLater(authenticationBloc.state, emitsInOrder(expectedStates)); 171 | authenticationBloc.dispatch(ClickedLogout()); 172 | }); 173 | }); 174 | 175 | test('emits no states after calling dispose', () { 176 | expectLater( 177 | authenticationBloc.state, 178 | emitsInOrder([]), 179 | ); 180 | authenticationBloc.dispose(); 181 | }); 182 | } 183 | -------------------------------------------------------------------------------- /test/main_test.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | testWidgets('Main UI Test', (WidgetTester tester) async { 6 | 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /test/mock/firebase_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:firebase_storage/firebase_storage.dart'; 4 | import 'package:google_sign_in/google_sign_in.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | 7 | /* Creating mock objects for all the firebase related classes 8 | we'll use these to try and recreate the flow of the functions in actual app 9 | Properties are overriden in places where they are used 10 | for example accessToken and idToken are passed on to AutheCredential object 11 | */ 12 | 13 | /* 14 | AuthenticationProvider Mocks 15 | */ 16 | 17 | class FirebaseAuthMock extends Mock implements FirebaseAuth{} 18 | class GoogleSignInMock extends Mock implements GoogleSignIn{} 19 | class GoogleSignInAccountMock extends Mock implements GoogleSignInAccount{} 20 | 21 | class GoogleSignInAuthenticationMock extends Mock implements GoogleSignInAuthentication{ 22 | @override 23 | String get accessToken => 'mock_access_token'; 24 | @override 25 | String get idToken => 'mock_id_token'; 26 | } 27 | 28 | class AuthCredentialMock extends Mock implements AuthCredential{} 29 | 30 | class FirebaseUserMock extends Mock implements User{ 31 | @override 32 | String get displayName => 'John Doe'; 33 | @override 34 | String get uid => 'uid'; 35 | @override 36 | String get email => 'johndoe@mail.com'; 37 | @override 38 | String get photoURL => 'http://www.adityag.me'; 39 | } 40 | 41 | /* 42 | StorageProvider Mocks 43 | */ 44 | 45 | class FirebaseStorageMock extends Mock implements FirebaseStorage{} 46 | class StorageReferenceMock extends Mock implements Reference{ 47 | StorageReferenceMock childReference; 48 | StorageReferenceMock({this.childReference}); 49 | @override 50 | Reference child(String path) { 51 | // TODO: implement child 52 | return childReference; 53 | } 54 | } 55 | class StorageUploadTaskMock extends Mock implements UploadTask{} 56 | class StorageTaskSnapshotMock extends Mock implements TaskSnapshot{} 57 | 58 | /* 59 | UserDataProvider Mocks 60 | */ 61 | class FireStoreMock extends Mock implements FirebaseFirestore{} 62 | 63 | 64 | class DocumentReferenceMock extends Mock implements DocumentReference{ 65 | DocumentSnapshotMock documentSnapshotMock; 66 | 67 | DocumentReferenceMock({this.documentSnapshotMock}); 68 | 69 | @override 70 | Future get({Source source = Source.serverAndCache}) { 71 | // TODO: implement get 72 | return Future.value(documentSnapshotMock); 73 | } 74 | @override 75 | Stream snapshots({bool includeMetadataChanges = false}) { 76 | if(documentSnapshotMock!=null) 77 | return Stream.fromFuture(Future.value(documentSnapshotMock)); 78 | else { 79 | return Stream.empty(); 80 | }} 81 | @override 82 | Future setData(Map data, {bool merge = false}) { 83 | if(this.documentSnapshotMock==null) 84 | this.documentSnapshotMock = DocumentSnapshotMock(); 85 | if(this.documentSnapshotMock.data==null){ 86 | documentSnapshotMock.data = Map(); 87 | } 88 | data.forEach((k,v){ 89 | documentSnapshotMock.mockData[k]=v; 90 | }); 91 | return null; 92 | } 93 | 94 | @override 95 | Future updateData(Map data) { 96 | if(this.documentSnapshotMock==null) 97 | this.documentSnapshotMock = DocumentSnapshotMock(); 98 | if(this.documentSnapshotMock.data==null){ 99 | documentSnapshotMock.data = Map(); 100 | } 101 | data.forEach((k,v){ 102 | documentSnapshotMock.mockData[k]=v; 103 | }); 104 | return null; 105 | } 106 | } 107 | class DocumentSnapshotMock extends Mock implements DocumentSnapshot{ 108 | Map mockData = Map(); 109 | DocumentSnapshotMock({this.mockData}); 110 | 111 | set data(Map data) => this.mockData = data; 112 | @override 113 | Map get data => mockData; 114 | @override 115 | bool get exists => true; 116 | } 117 | 118 | class CollectionReferenceMock extends Mock implements CollectionReference{} 119 | 120 | class QuerySnapshotMock extends Mock implements QuerySnapshot{} 121 | 122 | class QueryMock extends Mock implements Query{} -------------------------------------------------------------------------------- /test/mock/io_mock.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:mockito/mockito.dart'; 4 | 5 | class MockFile extends Mock implements File{} -------------------------------------------------------------------------------- /test/mock/repository_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:messio/repositories/authentication_repository.dart'; 2 | import 'package:messio/repositories/storage_repository.dart'; 3 | import 'package:messio/repositories/user_data_repository.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | 6 | class AuthenticationRepositoryMock extends Mock implements AuthenticationRepository{} 7 | class UserDataRepositoryMock extends Mock implements UserDataRepository{} 8 | class StorageRepositoryMock extends Mock implements StorageRepository{} -------------------------------------------------------------------------------- /test/mock/shared_objects_mock.dart: -------------------------------------------------------------------------------- 1 | import 'package:messio/utils/shared_objects.dart'; 2 | import 'package:mockito/mockito.dart'; 3 | 4 | class SharedPreferencesMock extends Mock implements CachedSharedPreferences{} -------------------------------------------------------------------------------- /test/pages/conversation_page_slide_test.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | void main(){ 4 | // const MaterialApp app = MaterialApp( 5 | // home: Scaffold( 6 | // body: const ConversationPageSlide() 7 | // ), 8 | // ); 9 | // testWidgets('ConversationPageSlide UI Test', (WidgetTester tester) async { 10 | // // Build our app and trigger a frame. 11 | // await tester.pumpWidget(app); 12 | // expect(find.byType(ConversationPage),findsOneWidget); 13 | // expect(find.byType(PageView),findsOneWidget); 14 | // expect(find.byType(InputWidget),findsOneWidget); 15 | // 16 | // }); 17 | } -------------------------------------------------------------------------------- /test/pages/conversation_page_test.dart: -------------------------------------------------------------------------------- 1 | 2 | void main(){ 3 | // const MaterialApp app = MaterialApp( 4 | // home: Scaffold( 5 | // body: const ConversationPage() 6 | // ), 7 | // ); 8 | // 9 | // testWidgets('ConversationPage UI Test', (WidgetTester tester) async { 10 | // // Build our app and trigger a frame. 11 | // await tester.pumpWidget(app); 12 | // 13 | // expect(find.byType(ChatAppBar),findsOneWidget); 14 | // expect(find.byType(ChatListWidget),findsOneWidget); 15 | // 16 | // 17 | // }); 18 | } -------------------------------------------------------------------------------- /test/providers/authentication_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:messio/config/constants.dart'; 3 | import 'package:messio/providers/authentication_provider.dart'; 4 | import 'package:messio/utils/shared_objects.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | import '../mock/firebase_mock.dart'; 7 | import '../mock/shared_objects_mock.dart'; 8 | 9 | void main() { 10 | group('AuthenticationProvider', () { 11 | //Mock and inject the basic dependencies in the AuthenticationProvider 12 | FirebaseAuthMock firebaseAuth = FirebaseAuthMock(); 13 | GoogleSignInMock googleSignIn = GoogleSignInMock(); 14 | 15 | AuthenticationProvider authenticationProvider = AuthenticationProvider( 16 | firebaseAuth: firebaseAuth, googleSignIn: googleSignIn); 17 | 18 | //Mock rest of the objects needed to replicate the AuthenticationProvider functions 19 | final GoogleSignInAccountMock googleSignInAccount = 20 | GoogleSignInAccountMock(); 21 | final GoogleSignInAuthenticationMock googleSignInAuthentication = 22 | GoogleSignInAuthenticationMock(); 23 | final FirebaseUserMock firebaseUser = FirebaseUserMock(); 24 | SharedPreferencesMock sharedPreferencesMock = SharedPreferencesMock(); 25 | SharedObjects.prefs = sharedPreferencesMock; 26 | 27 | test('signInWithGoogle returns a Firebase user', () async { 28 | //mock the method calls 29 | when(sharedPreferencesMock.getString(any)).thenReturn('uid'); 30 | when(SharedObjects.prefs.setString(any, any)).thenAnswer((_)=>Future.value(true)); 31 | when(googleSignIn.signIn()).thenAnswer( 32 | (_) => Future.value(googleSignInAccount)); 33 | when(googleSignInAccount.authentication).thenAnswer((_) => 34 | Future.value( 35 | googleSignInAuthentication)); 36 | when(firebaseAuth.currentUser()) 37 | .thenAnswer((_) => Future.value(firebaseUser)); 38 | 39 | //call the method and expect the Firebase user as return 40 | expect(await authenticationProvider.signInWithGoogle(), firebaseUser); 41 | verify(googleSignIn.signIn()).called(1); 42 | verify(googleSignInAccount.authentication).called(1); 43 | }); 44 | 45 | test('getCurrentUser returns current user', () async { 46 | when(firebaseAuth.currentUser()) 47 | .thenAnswer((_) => Future.value(firebaseUser)); 48 | expect(await authenticationProvider.getCurrentUser(), firebaseUser); 49 | }); 50 | 51 | test('isLoggedIn return true only when FirebaseAuth has a user', () async { 52 | when(firebaseAuth.currentUser()) 53 | .thenAnswer((_) => Future.value(firebaseUser)); 54 | expect(await authenticationProvider.isLoggedIn(), true); 55 | when(firebaseAuth.currentUser()) 56 | .thenAnswer((_) => Future.value(null)); 57 | expect(await authenticationProvider.isLoggedIn(), false); 58 | }); 59 | 60 | test('signOutUser clears the session', () async { 61 | when(sharedPreferencesMock.getString(Constants.sessionUsername)).thenReturn('username'); 62 | expect(SharedObjects.prefs.getString(Constants.sessionUsername),'username'); 63 | 64 | //mocking all the methods use by signOutUser 65 | when(firebaseAuth.signOut()).thenAnswer((_)=>Future(null)); 66 | when(googleSignIn.signOut()).thenAnswer((_)=>Future.value(googleSignInAccount)); 67 | when(sharedPreferencesMock.clearSession()).thenAnswer((_){ 68 | when(sharedPreferencesMock.getString(Constants.sessionUsername)).thenReturn(null); 69 | return; 70 | }); 71 | authenticationProvider.signOutUser(); 72 | expect(SharedObjects.prefs.getString(Constants.sessionUsername),null); 73 | }); 74 | 75 | }); 76 | } 77 | 78 | -------------------------------------------------------------------------------- /test/providers/storage_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:messio/providers/storage_provider.dart'; 3 | import 'package:messio/utils/shared_objects.dart'; 4 | import 'package:mockito/mockito.dart'; 5 | 6 | import '../mock/firebase_mock.dart'; 7 | import '../mock/io_mock.dart'; 8 | import '../mock/shared_objects_mock.dart'; 9 | void main() { 10 | group('StorageProvider', () { 11 | FirebaseStorageMock firebaseStorage = FirebaseStorageMock(); //Create the mock objects required 12 | StorageReferenceMock storageReference = StorageReferenceMock(); 13 | StorageReferenceMock rootReference = 14 | StorageReferenceMock(childReference: storageReference); 15 | StorageReferenceMock fileReference = StorageReferenceMock(); 16 | StorageUploadTaskMock storageUploadTask = StorageUploadTaskMock(); 17 | StorageTaskSnapshotMock storageTaskSnapshot = StorageTaskSnapshotMock(); 18 | MockFile mockFile = MockFile(); 19 | String resultUrl = "http://www.adityag.me/"; 20 | StorageProvider storageProvider = 21 | StorageProvider(firebaseStorage: firebaseStorage); 22 | 23 | test('Testing if uploadImage returns a url', () async { 24 | SharedPreferencesMock sharedPreferencesMock = SharedPreferencesMock(); 25 | SharedObjects.prefs = sharedPreferencesMock; 26 | when(sharedPreferencesMock.getString(any)).thenReturn('uid'); 27 | when(SharedObjects.prefs.setString(any, any)).thenAnswer((_)=>Future.value(true)); 28 | when(mockFile.path).thenReturn("/storage/file.jpg"); //this is necessary because basename() method from path.dart uses the path of the file to return its basename 29 | when(firebaseStorage.ref()).thenReturn(rootReference); 30 | when(storageReference.putFile(any)).thenReturn(storageUploadTask); 31 | when(storageUploadTask.onComplete).thenAnswer( 32 | (_) => Future.value(storageTaskSnapshot)); 33 | when(storageTaskSnapshot.ref).thenReturn(fileReference); 34 | when(fileReference.getDownloadURL()) 35 | .thenAnswer((_) => Future.value(resultUrl)); 36 | 37 | expect(await storageProvider.uploadFile(mockFile, ''), resultUrl); 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/widgets/chat_app_bar_test.dart: -------------------------------------------------------------------------------- 1 | 2 | void main(){ 3 | // const MaterialApp app = MaterialApp( 4 | // home: Scaffold( 5 | // body: const ChatAppBar() 6 | // ), 7 | // ); 8 | // testWidgets('ChatAppBar UI Test', (WidgetTester tester) async { 9 | // // Build our app and trigger a frame. 10 | // await tester.pumpWidget(app); 11 | // 12 | // expect(find.text('Aditya Gurjar'), findsOneWidget); 13 | // expect(find.text('@adityagurjar'), findsOneWidget); 14 | // expect(find.byType(IconButton),findsNWidgets(1)); 15 | // expect(find.byType(CircleAvatar),findsOneWidget); 16 | // }); 17 | } -------------------------------------------------------------------------------- /test/widgets/chat_item_widget_test.dart: -------------------------------------------------------------------------------- 1 | 2 | void main(){ 3 | // const MaterialApp app = MaterialApp( 4 | // home: Scaffold( 5 | // body: const ChatItemWidget() 6 | // ), 7 | // ); 8 | // testWidgets('ChatItemWidget UI Test', (WidgetTester tester) async { 9 | // // Build our app and trigger a frame. 10 | // await tester.pumpWidget(app); 11 | // 12 | // expect(find.byType(Container),findsNWidgets(3)); 13 | // expect(find.byType(Column),findsNWidgets(1)); 14 | // expect(find.byType(Row),findsNWidgets(2)); 15 | // expect(find.byType(Text),findsNWidgets(2)); 16 | // }); 17 | } -------------------------------------------------------------------------------- /test/widgets/chat_list_widget_test.dart: -------------------------------------------------------------------------------- 1 | 2 | void main(){ 3 | // MaterialApp app = MaterialApp( 4 | // home: Scaffold( 5 | // body: ChatListWidget() 6 | // ), 7 | // ); 8 | // testWidgets('ChatListWidget UI Test', (WidgetTester tester) async { 9 | // // Build our app and trigger a frame. 10 | // await tester.pumpWidget(app); 11 | // expect(find.byType(ListView),findsOneWidget); 12 | // 13 | // }); 14 | } -------------------------------------------------------------------------------- /test/widgets/input_widget_test.dart: -------------------------------------------------------------------------------- 1 | 2 | void main(){ 3 | // MaterialApp app = MaterialApp( 4 | // home: Scaffold( 5 | // body: InputWidget() 6 | // ), 7 | // ); 8 | // testWidgets('InputWidget UI Test', (WidgetTester tester) async { 9 | // // Build our app and trigger a frame. 10 | // await tester.pumpWidget(app); 11 | // 12 | // expect(find.byType(IconButton),findsNWidgets(2)); 13 | // expect(find.byType(EditableText),findsOneWidget); 14 | // 15 | // }); 16 | } --------------------------------------------------------------------------------