├── .idea ├── .gitignore ├── flutter_chat_app_with_nodejs.iml ├── modules.xml └── vcs.xml ├── LICENSE ├── README.md ├── flutter_app ├── .gitignore ├── .metadata ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── flutter_app │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.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-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── assets │ └── fonts │ │ └── RedHatDisplay │ │ ├── OFL.txt │ │ ├── README.txt │ │ ├── RedHatDisplay-Black.ttf │ │ ├── RedHatDisplay-BlackItalic.ttf │ │ ├── RedHatDisplay-Bold.ttf │ │ ├── RedHatDisplay-BoldItalic.ttf │ │ ├── RedHatDisplay-ExtraBold.ttf │ │ ├── RedHatDisplay-ExtraBoldItalic.ttf │ │ ├── RedHatDisplay-Italic-VariableFont_wght.ttf │ │ ├── RedHatDisplay-Italic.ttf │ │ ├── RedHatDisplay-Light.ttf │ │ ├── RedHatDisplay-LightItalic.ttf │ │ ├── RedHatDisplay-Medium.ttf │ │ ├── RedHatDisplay-MediumItalic.ttf │ │ ├── RedHatDisplay-Regular.ttf │ │ ├── RedHatDisplay-SemiBold.ttf │ │ ├── RedHatDisplay-SemiBoldItalic.ttf │ │ └── RedHatDisplay-VariableFont_wght.ttf ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ └── LaunchImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ └── README.md │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ └── RunnerTests │ │ └── RunnerTests.swift ├── lib │ ├── core │ │ ├── data │ │ │ ├── data_sources │ │ │ │ ├── auth_local_ds.dart │ │ │ │ ├── connection_remote_ds.dart │ │ │ │ ├── hive_box_instance.dart │ │ │ │ ├── users_local_ds.dart │ │ │ │ └── users_remote_ds.dart │ │ │ └── repositories │ │ │ │ ├── auth_repo_impl.dart │ │ │ │ ├── connection_repo_impl.dart │ │ │ │ └── users_repo_impl.dart │ │ ├── domain │ │ │ ├── entities │ │ │ │ └── failures │ │ │ │ │ ├── failure.dart │ │ │ │ │ └── timeout_failure.dart │ │ │ ├── repositories │ │ │ │ ├── auth_repo.dart │ │ │ │ ├── connection_repo.dart │ │ │ │ └── users_repo.dart │ │ │ └── use_cases │ │ │ │ ├── initialize_app.dart │ │ │ │ ├── logout.dart │ │ │ │ ├── stream_connection_changes.dart │ │ │ │ └── stream_users_to_talk.dart │ │ ├── utils │ │ │ ├── dartz_utils.dart │ │ │ ├── formatted_text.dart │ │ │ ├── snackbar.dart │ │ │ └── validators.dart │ │ └── widgets │ │ │ ├── button_widget.dart │ │ │ ├── center_content_widget.dart │ │ │ ├── connection_status_widget.dart │ │ │ ├── expanded_section_widget.dart │ │ │ ├── my_appbar_widget.dart │ │ │ ├── my_custom_text_form_field.dart │ │ │ ├── my_multiline_text_field.dart │ │ │ ├── stopwatch │ │ │ ├── stopwatch_controller.dart │ │ │ └── stopwatch_widget.dart │ │ │ └── waves_background │ │ │ ├── clipper │ │ │ └── waves_background_clipper.dart │ │ │ └── waves_background.dart │ ├── environment.dart │ ├── features │ │ ├── call │ │ │ └── presentation │ │ │ │ └── screens │ │ │ │ └── call_screen.dart │ │ ├── chat │ │ │ ├── data │ │ │ │ ├── data_sources │ │ │ │ │ ├── messages_local_ds.dart │ │ │ │ │ └── messages_remote_ds.dart │ │ │ │ ├── models │ │ │ │ │ ├── message_model.dart │ │ │ │ │ ├── sending_text_message_model.dart │ │ │ │ │ ├── sending_typing_model.dart │ │ │ │ │ └── user_model.dart │ │ │ │ ├── repositories │ │ │ │ │ └── messages_repo_impl.dart │ │ │ │ └── utils.dart │ │ │ ├── domain │ │ │ │ ├── entities │ │ │ │ │ ├── chat_content_entity.dart │ │ │ │ │ ├── chat_list_item_entity.dart │ │ │ │ │ ├── conversation_entity.dart │ │ │ │ │ ├── last_chat_messages_from_each_user_result.dart │ │ │ │ │ ├── message_entity.dart │ │ │ │ │ ├── model_source.dart │ │ │ │ │ ├── pending_request_to_api_telling_message_was_read.dart │ │ │ │ │ ├── sending_text_message_entity.dart │ │ │ │ │ ├── sending_typing_entity.dart │ │ │ │ │ └── user_entity.dart │ │ │ │ ├── repositories │ │ │ │ │ └── messages_repo.dart │ │ │ │ └── use_cases │ │ │ │ │ ├── listen_to_conversations.dart │ │ │ │ │ ├── messages_stream.dart │ │ │ │ │ ├── notify_logged_user_is_typing.dart │ │ │ │ │ └── send_message.dart │ │ │ └── presentation │ │ │ │ ├── controllers │ │ │ │ ├── logout_controller.dart │ │ │ │ ├── realtime_chat_page_controller.dart │ │ │ │ ├── send_message_controller.dart │ │ │ │ └── users_to_talk_to_controller.dart │ │ │ │ ├── screens │ │ │ │ ├── realtime_chat_screen │ │ │ │ │ └── realtime_chat_screen.dart │ │ │ │ └── realtime_conversations_screen │ │ │ │ │ └── realtime_conversations_screen.dart │ │ │ │ └── widgets │ │ │ │ ├── balloon_widget.dart │ │ │ │ ├── chat_item_widget.dart │ │ │ │ ├── delay_animate_switcher.dart │ │ │ │ ├── load_more_messages_button.dart │ │ │ │ ├── logout_button_widget.dart │ │ │ │ ├── message_status_widget.dart │ │ │ │ ├── message_widget.dart │ │ │ │ ├── separator_date_for_messages_widget.dart │ │ │ │ └── typing_indicator_widget.dart │ │ ├── loading │ │ │ └── screens │ │ │ │ └── loading_screen.dart │ │ └── login_and_registration │ │ │ ├── data │ │ │ ├── data_sources │ │ │ │ └── auth_remote_ds.dart │ │ │ └── models │ │ │ │ └── tokens_model.dart │ │ │ ├── domain │ │ │ ├── entities │ │ │ │ ├── failures │ │ │ │ │ ├── credential_failure.dart │ │ │ │ │ ├── email_already_exists_failure.dart │ │ │ │ │ ├── invalid_email_failure.dart │ │ │ │ │ ├── invalid_password_failure.dart │ │ │ │ │ └── invalid_refresh_token_failure.dart │ │ │ │ └── tokens_entity.dart │ │ │ └── use_cases │ │ │ │ ├── login.dart │ │ │ │ └── register.dart │ │ │ └── screens │ │ │ ├── content │ │ │ ├── login_content.dart │ │ │ └── register_content.dart │ │ │ ├── login_and_registration_screen.dart │ │ │ └── widgets │ │ │ └── icon │ │ │ └── animated_icon.dart │ ├── injection_container.dart │ ├── main.dart │ └── screen_routes.dart ├── linux │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── macos │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Podfile │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── app_icon_1024.png │ │ │ │ ├── app_icon_128.png │ │ │ │ ├── app_icon_16.png │ │ │ │ ├── app_icon_256.png │ │ │ │ ├── app_icon_32.png │ │ │ │ ├── app_icon_512.png │ │ │ │ └── app_icon_64.png │ │ ├── Base.lproj │ │ │ └── MainMenu.xib │ │ ├── Configs │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements │ └── RunnerTests │ │ └── RunnerTests.swift ├── pubspec.lock ├── pubspec.yaml ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ ├── Icon-512.png │ │ ├── Icon-maskable-192.png │ │ └── Icon-maskable-512.png │ ├── index.html │ └── manifest.json └── windows │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake │ └── runner │ ├── CMakeLists.txt │ ├── Runner.rc │ ├── flutter_window.cpp │ ├── flutter_window.h │ ├── main.cpp │ ├── resource.h │ ├── resources │ └── app_icon.ico │ ├── runner.exe.manifest │ ├── utils.cpp │ ├── utils.h │ ├── win32_window.cpp │ └── win32_window.h └── nodejs_websocket_backend ├── .gitignore ├── package-lock.json ├── package.json ├── src ├── controllers │ ├── auth-controller │ │ └── auth-controller.ts │ ├── message-controller │ │ ├── message-controller.ts │ │ └── routes │ │ │ ├── conversations-with-unreceived-messages-route.ts │ │ │ ├── create-message-route.ts │ │ │ ├── messages-were-read-route.ts │ │ │ ├── messages-were-updated-route.ts │ │ │ ├── read-messages-route.ts │ │ │ ├── typing-route.ts │ │ │ └── user-typed-route.ts │ └── user-controller │ │ └── user-controller.ts ├── data │ ├── data-source │ │ └── db-datasouce.ts │ └── models │ │ ├── chat-content-model.ts │ │ ├── message-model.ts │ │ ├── tokens-model.ts │ │ └── user-model.ts ├── domain │ ├── controllers-and-services.ts │ ├── entity │ │ ├── chat-content-entity.ts │ │ ├── failures │ │ │ ├── duplicate-email-failure.ts │ │ │ ├── failure.ts │ │ │ ├── invalid-email-failure.ts │ │ │ ├── invalid-password-failure.ts │ │ │ └── invalid-refresh-token-failure.ts │ │ ├── text-message-entity.ts │ │ ├── tokens-entity.ts │ │ ├── typing-indicator-entity.ts │ │ └── user-entity.ts │ └── services │ │ ├── auth-service.ts │ │ ├── messages-service.ts │ │ └── users-service.ts ├── environment │ ├── db.ts │ └── jwt-private.key ├── index.ts └── utils │ ├── either.ts │ ├── encryption-utils.ts │ └── jwt-utils.ts └── tsconfig.json /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rodrigo João Bertotti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /flutter_app/.gitignore: -------------------------------------------------------------------------------- 1 | lib/environment.dart 2 | 3 | # Miscellaneous 4 | *.class 5 | *.log 6 | *.pyc 7 | *.swp 8 | .DS_Store 9 | .atom/ 10 | .buildlog/ 11 | .history 12 | .svn/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | #**/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | /build/ 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /flutter_app/.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: "ba393198430278b6595976de84fe170f553cc728" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: ba393198430278b6595976de84fe170f553cc728 17 | base_revision: ba393198430278b6595976de84fe170f553cc728 18 | - platform: android 19 | create_revision: ba393198430278b6595976de84fe170f553cc728 20 | base_revision: ba393198430278b6595976de84fe170f553cc728 21 | - platform: ios 22 | create_revision: ba393198430278b6595976de84fe170f553cc728 23 | base_revision: ba393198430278b6595976de84fe170f553cc728 24 | - platform: linux 25 | create_revision: ba393198430278b6595976de84fe170f553cc728 26 | base_revision: ba393198430278b6595976de84fe170f553cc728 27 | - platform: macos 28 | create_revision: ba393198430278b6595976de84fe170f553cc728 29 | base_revision: ba393198430278b6595976de84fe170f553cc728 30 | - platform: web 31 | create_revision: ba393198430278b6595976de84fe170f553cc728 32 | base_revision: ba393198430278b6595976de84fe170f553cc728 33 | - platform: windows 34 | create_revision: ba393198430278b6595976de84fe170f553cc728 35 | base_revision: ba393198430278b6595976de84fe170f553cc728 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /flutter_app/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /flutter_app/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /flutter_app/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "com.example.flutter_app" 27 | compileSdk 34 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 45 | applicationId "com.example.flutter_app" 46 | // You can update the following values to match your application needs. 47 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 48 | minSdkVersion 21 49 | targetSdkVersion 34 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | 54 | buildTypes { 55 | release { 56 | // TODO: Add your own signing config for the release build. 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } 66 | 67 | dependencies {} 68 | -------------------------------------------------------------------------------- /flutter_app/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter_app/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /flutter_app/android/app/src/main/kotlin/com/example/flutter_app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.flutter_app 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /flutter_app/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /flutter_app/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /flutter_app/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /flutter_app/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter_app/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /flutter_app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /flutter_app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip 6 | -------------------------------------------------------------------------------- /flutter_app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 22 | id "com.android.application" version "7.3.0" apply false 23 | id "org.jetbrains.kotlin.android" version "1.8.20" apply false 24 | } 25 | 26 | include ":app" 27 | -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/README.txt: -------------------------------------------------------------------------------- 1 | Red Hat Display Variable Font 2 | ============================= 3 | 4 | This download contains Red Hat Display as both variable fonts and static fonts. 5 | 6 | Red Hat Display is a variable font with this axis: 7 | wght 8 | 9 | This means all the styles are contained in these files: 10 | RedHatDisplay-VariableFont_wght.ttf 11 | RedHatDisplay-Italic-VariableFont_wght.ttf 12 | 13 | If your app fully supports variable fonts, you can now pick intermediate styles 14 | that aren’t available as static fonts. Not all apps support variable fonts, and 15 | in those cases you can use the static font files for Red Hat Display: 16 | static/RedHatDisplay-Light.ttf 17 | static/RedHatDisplay-Regular.ttf 18 | static/RedHatDisplay-Medium.ttf 19 | static/RedHatDisplay-SemiBold.ttf 20 | static/RedHatDisplay-Bold.ttf 21 | static/RedHatDisplay-ExtraBold.ttf 22 | static/RedHatDisplay-Black.ttf 23 | static/RedHatDisplay-LightItalic.ttf 24 | static/RedHatDisplay-Italic.ttf 25 | static/RedHatDisplay-MediumItalic.ttf 26 | static/RedHatDisplay-SemiBoldItalic.ttf 27 | static/RedHatDisplay-BoldItalic.ttf 28 | static/RedHatDisplay-ExtraBoldItalic.ttf 29 | static/RedHatDisplay-BlackItalic.ttf 30 | 31 | Get started 32 | ----------- 33 | 34 | 1. Install the font files you want to use 35 | 36 | 2. Use your app's font picker to view the font family and all the 37 | available styles 38 | 39 | Learn more about variable fonts 40 | ------------------------------- 41 | 42 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts 43 | https://variablefonts.typenetwork.com 44 | https://medium.com/variable-fonts 45 | 46 | In desktop apps 47 | 48 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc 49 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts 50 | 51 | Online 52 | 53 | https://developers.google.com/fonts/docs/getting_started 54 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide 55 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts 56 | 57 | Installing fonts 58 | 59 | MacOS: https://support.apple.com/en-us/HT201749 60 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux 61 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows 62 | 63 | Android Apps 64 | 65 | https://developers.google.com/fonts/docs/android 66 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts 67 | 68 | License 69 | ------- 70 | Please read the full license text (OFL.txt) to understand the permissions, 71 | restrictions and requirements for usage, redistribution, and modification. 72 | 73 | You can use them in your products & projects – print or digital, 74 | commercial or otherwise. 75 | 76 | This isn't legal advice, please consider consulting a lawyer and see the full 77 | license for all details. 78 | -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Black.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-BlackItalic.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Bold.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-BoldItalic.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-ExtraBold.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Italic-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Italic-VariableFont_wght.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Italic.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Light.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-LightItalic.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Medium.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-MediumItalic.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-Regular.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-SemiBold.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/assets/fonts/RedHatDisplay/RedHatDisplay-VariableFont_wght.ttf -------------------------------------------------------------------------------- /flutter_app/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /flutter_app/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /flutter_app/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /flutter_app/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /flutter_app/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | target.build_configurations.each do |config| 44 | # Workaround for https://github.com/flutter/flutter/issues/64502 45 | config.build_settings['ONLY_ACTIVE_ARCH'] = 'YES' # <= this line 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /flutter_app/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - connectivity_plus (0.0.1): 3 | - Flutter 4 | - ReachabilitySwift 5 | - DTTJailbreakDetection (0.4.0) 6 | - Flutter (1.0.0) 7 | - flutter_keyboard_visibility (0.0.1): 8 | - Flutter 9 | - flutter_webrtc (0.9.36): 10 | - Flutter 11 | - WebRTC-SDK (= 114.5735.02) 12 | - path_provider_foundation (0.0.1): 13 | - Flutter 14 | - FlutterMacOS 15 | - ReachabilitySwift (5.0.0) 16 | - safe_device (1.0.0): 17 | - DTTJailbreakDetection 18 | - Flutter 19 | - WebRTC-SDK (114.5735.02) 20 | 21 | DEPENDENCIES: 22 | - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) 23 | - Flutter (from `Flutter`) 24 | - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) 25 | - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) 26 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 27 | - safe_device (from `.symlinks/plugins/safe_device/ios`) 28 | 29 | SPEC REPOS: 30 | trunk: 31 | - DTTJailbreakDetection 32 | - ReachabilitySwift 33 | - WebRTC-SDK 34 | 35 | EXTERNAL SOURCES: 36 | connectivity_plus: 37 | :path: ".symlinks/plugins/connectivity_plus/ios" 38 | Flutter: 39 | :path: Flutter 40 | flutter_keyboard_visibility: 41 | :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" 42 | flutter_webrtc: 43 | :path: ".symlinks/plugins/flutter_webrtc/ios" 44 | path_provider_foundation: 45 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 46 | safe_device: 47 | :path: ".symlinks/plugins/safe_device/ios" 48 | 49 | SPEC CHECKSUMS: 50 | connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a 51 | DTTJailbreakDetection: 5e356c5badc17995f65a83ed9483f787a0057b71 52 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 53 | flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 54 | flutter_webrtc: 1944895d4e908c4bc722929dc4b9f8620d8e1b2f 55 | path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 56 | ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 57 | safe_device: 4539eb6bdbeb4b61a763a51c4e73e6b37dea4e3d 58 | WebRTC-SDK: dd913fd31cfbf1d43b9a22d83f4c6354c960c623 59 | 60 | PODFILE CHECKSUM: 78e6df5e729af295e7ef6095bb64cbfff1a5b1b0 61 | 62 | COCOAPODS: 1.11.3 63 | -------------------------------------------------------------------------------- /flutter_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter_app/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /flutter_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter_app/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Flutter Chat App With Mysql 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | flutter_chat_app_with_mysql 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | NSCameraUsageDescription 30 | $(PRODUCT_NAME) Camera Usage! 31 | NSMicrophoneUsageDescription 32 | $(PRODUCT_NAME) Microphone Usage! 33 | UIApplicationSupportsIndirectInputEvents 34 | 35 | UILaunchStoryboardName 36 | LaunchScreen 37 | UIMainStoryboardFile 38 | Main 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | UISupportedInterfaceOrientations~ipad 46 | 47 | UIInterfaceOrientationPortrait 48 | UIInterfaceOrientationPortraitUpsideDown 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /flutter_app/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /flutter_app/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /flutter_app/lib/core/data/data_sources/auth_local_ds.dart: -------------------------------------------------------------------------------- 1 | import 'hive_box_instance.dart'; 2 | 3 | 4 | class AuthLocalDS { 5 | static const String _kLoggedUserId = "loggedUserId"; 6 | static const String _kAccessToken = "accessToken"; 7 | static const String _kAccessTokenExpiration = "accessTokenExpiration"; 8 | static const String _kRefreshToken = "refreshToken"; 9 | 10 | late final HiveBoxInstance _hive; 11 | 12 | AuthLocalDS({required HiveBoxInstance hiveBoxInstance}) { 13 | _hive = hiveBoxInstance; 14 | } 15 | 16 | int? get loggedUserId { 17 | final loggedUserId = _hive.box.get(_kLoggedUserId); 18 | if(loggedUserId == null){ 19 | return null; 20 | } 21 | return int.parse(loggedUserId); 22 | } 23 | 24 | String? getAccessToken () => _hive.box.get(_kAccessToken); 25 | String? getRefreshToken () => _hive.box.get(_kRefreshToken); 26 | DateTime? getAccessTokenExpiration () => _hive.box.get(_kAccessTokenExpiration); 27 | 28 | Future setLoggedUserId ({required int loggedUserId}) async => _hive.box.put(_kLoggedUserId, loggedUserId.toString()); 29 | Future setAccessToken ({required String accessToken}) async => _hive.box.put(_kAccessToken, accessToken); 30 | Future setRefreshToken ({required String refreshToken}) async => _hive.box.put(_kRefreshToken, refreshToken); 31 | Future setAccessTokenExpiration ({required DateTime accessTokenExpiration}) async => _hive.box.put(_kAccessTokenExpiration, accessTokenExpiration); 32 | 33 | Future deleteAccessToken () async => _hive.box.delete(_kAccessToken); 34 | Future deleteRefreshToken () async => _hive.box.delete(_kRefreshToken); 35 | Future deleteLoggedUserId () async => _hive.box.delete(_kLoggedUserId); 36 | Future deleteAccessTokenExpiration () async => _hive.box.delete(_kAccessTokenExpiration); 37 | 38 | Future clear () async { 39 | await deleteAccessToken(); 40 | await deleteAccessTokenExpiration(); 41 | await deleteRefreshToken(); 42 | await deleteLoggedUserId(); 43 | } 44 | 45 | 46 | 47 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/data/data_sources/connection_remote_ds.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import 'package:askless/index.dart'; 5 | 6 | class ConnectionRemoteDS { 7 | final String serverUrl = "ws://192.168.0.8:3000"; // TODO: Replace with your websocket server URL here (e.g. IPv4) 8 | 9 | void start({required OnAutoReauthenticationFails onAutoReauthenticationFails}) { 10 | AsklessClient.instance.start( 11 | serverUrl: serverUrl, 12 | debugLogs: false, 13 | getWebRTCParams: (userId) => Future.value( 14 | WebRTCParams(configuration: { 15 | 'iceServers': [ 16 | { 17 | "urls": [ 18 | 'stun:stun1.l.google.com:19302', 19 | 'stun:stun2.l.google.com:19302' 20 | ], 21 | } 22 | ] 23 | }) 24 | ), 25 | onAutoReauthenticationFails: (String credentialErrorCode, void Function() clearAuthentication) { 26 | print("Credential failed with credentialErrorCode = $credentialErrorCode"); 27 | onAutoReauthenticationFails(credentialErrorCode, clearAuthentication); 28 | }, 29 | ); 30 | } 31 | 32 | 33 | Stream streamConnectionChanges({bool immediately = false}) { 34 | // Converting Askless Connection status (Connection enum) to this App connection status (ConnectionStatus enum) 35 | // This separation is good so the upper layers (repository, use cases, widgets) don't rely on the 36 | // data source implementation 37 | 38 | return AsklessClient.instance.streamConnectionChanges(immediately: immediately); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /flutter_app/lib/core/data/data_sources/hive_box_instance.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:hive_flutter/hive_flutter.dart'; 3 | import 'package:crypto/crypto.dart'; 4 | 5 | class HiveBoxInstance { 6 | late final String _localStorageEncryptionKey; 7 | Box? _box; 8 | 9 | HiveBoxInstance ({required String localStorageEncryptionKey}) { 10 | _localStorageEncryptionKey = localStorageEncryptionKey; 11 | } 12 | 13 | Box get box { 14 | assert(_box != null, "Ops! You should call init() and wait for it result before trying to get the box"); 15 | return _box!; 16 | } 17 | 18 | Future initialize() async { 19 | if (_box != null){ 20 | return _box!; 21 | } 22 | await Hive.initFlutter(); 23 | return _box = await Hive.openBox("my-flutter-client-example-box", encryptionCipher: HiveAesCipher(sha256.convert(utf8.encode(_localStorageEncryptionKey)).bytes)); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/data/data_sources/users_local_ds.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_chat_app_with_mysql/core/data/data_sources/hive_box_instance.dart'; 2 | import 'package:flutter_chat_app_with_mysql/features/chat/data/models/user_model.dart'; 3 | 4 | class UsersLocalDS { 5 | static const String _kUsersKeyPrefix = "user_"; 6 | static const String _kUsersIdsSaved = "usersIdsSaved"; 7 | 8 | late final HiveBoxInstance _hive; 9 | 10 | UsersLocalDS({required HiveBoxInstance hiveBoxInstance}) { 11 | _hive = hiveBoxInstance; 12 | } 13 | 14 | UserModel? readUserLocally(int userId) { 15 | final res = _hive.box.get(_getUserKey(userId)); 16 | if (res == null){ 17 | return null; 18 | } 19 | return UserModel.fromMap(res); 20 | } 21 | 22 | List? getUsersToTalkLocally() { 23 | final List res = []; 24 | final userIds = _hive.box.get(_kUsersIdsSaved); 25 | if(userIds == null) { 26 | return null; 27 | } 28 | for(final userId in List.from(userIds)) { 29 | final user = _hive.box.get(_getUserKey(userId)); 30 | assert(user != null); 31 | res.add(UserModel.fromMap(user)); 32 | } 33 | return res..sort((a,b){ 34 | return a.fullName.toLowerCase().compareTo(b.fullName.toLowerCase()); 35 | }); 36 | } 37 | 38 | String _getUserKey(int userId) => "$_kUsersKeyPrefix$userId"; 39 | 40 | Future saveUsersLocally(List usersRemoteRes) async { 41 | final usersIds = List.from(_hive.box.get(_kUsersIdsSaved, defaultValue: [])); 42 | for (final user in usersRemoteRes) { 43 | await _hive.box.put(_getUserKey(user.userId), user.toMap()); 44 | if(!usersIds.contains(user.userId)) { 45 | usersIds.add(user.userId); 46 | } 47 | } 48 | _hive.box.put(_kUsersIdsSaved, usersIds); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/data/data_sources/users_remote_ds.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer'; 3 | import 'package:askless/index.dart'; 4 | import 'package:dartz/dartz.dart'; 5 | import 'package:flutter_chat_app_with_mysql/features/login_and_registration/domain/entities/failures/email_already_exists_failure.dart'; 6 | import 'package:flutter_chat_app_with_mysql/core/domain/entities/failures/failure.dart'; 7 | import '../../../features/chat/data/models/user_model.dart'; 8 | 9 | class UsersRemoteDS { 10 | 11 | /// Fetches for the users 12 | Stream> streamUsersToTalk() { 13 | final stream = AsklessClient.instance.readStream(route: 'user-list', source: StreamSource.cacheAndRemote); 14 | return stream.map((output) => UserModel.fromList(output)); 15 | } 16 | 17 | /// Fetches for the users 18 | Future> readUsersToTalk() async { 19 | final res = await AsklessClient.instance.read(route: 'user-list',); 20 | if (res.success) { 21 | return UserModel.fromList(res.output); 22 | } 23 | throw Failure("${res.error!.code}: ${res.error!.description}"); 24 | } 25 | 26 | /// Creates a new user 27 | /// 28 | /// Throws [EmailAlreadyExistsFailure] if the [email] is already in use 29 | Future createUser({required String firstName, required String lastName, required String email, required String password}) async { 30 | final res = await AsklessClient.instance.create( 31 | route: 'user', 32 | body: { 33 | "firstName": firstName, 34 | "lastName": lastName, 35 | "email": email, 36 | "password": password, 37 | } 38 | ); 39 | if(res.success){ 40 | return UserModel.fromMap(res.output); 41 | } 42 | log("createUser: Error occurred with code ${res.error!.code} and description ${res.error!.description}"); 43 | if(res.error!.code == "DUPLICATED_EMAIL"){ 44 | throw EmailAlreadyExistsFailure(); 45 | } 46 | throw Failure(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /flutter_app/lib/core/data/repositories/connection_repo_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:askless/index.dart'; 2 | import 'package:flutter_chat_app_with_mysql/core/data/data_sources/connection_remote_ds.dart'; 3 | import 'package:flutter_chat_app_with_mysql/core/domain/repositories/auth_repo.dart'; 4 | import '../../../injection_container.dart'; 5 | import '../../domain/repositories/connection_repo.dart'; 6 | 7 | class ConnectionRepoImpl extends ConnectionRepo { 8 | final ConnectionRemoteDS connectionRemoteDS; 9 | 10 | ConnectionRepoImpl({required this.connectionRemoteDS,}); 11 | 12 | @override 13 | Stream streamConnectionChanges({bool immediately = false}) { 14 | return connectionRemoteDS.streamConnectionChanges(immediately: immediately); 15 | } 16 | 17 | @override 18 | void start ({required void Function() onAutoReauthenticationFails}) { 19 | connectionRemoteDS.start( 20 | onAutoReauthenticationFails: (credentialErrorCode, clearAuthentication) => getIt.get().onAutoReauthenticationFails(onAutoReauthenticationFails: onAutoReauthenticationFails), 21 | ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /flutter_app/lib/core/domain/entities/failures/failure.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | class Failure extends Error { 5 | dynamic _err; 6 | 7 | Failure([dynamic err]) { 8 | _err = err; 9 | } 10 | 11 | String get error => _err ?? "An error occurred, please try again later"; 12 | 13 | @override 14 | String toString() { 15 | return "Failure: $error"; 16 | } 17 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/domain/entities/failures/timeout_failure.dart: -------------------------------------------------------------------------------- 1 | import 'failure.dart'; 2 | 3 | class TimeoutFailure extends Failure {} -------------------------------------------------------------------------------- /flutter_app/lib/core/domain/repositories/auth_repo.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import '../entities/failures/failure.dart'; 3 | 4 | enum ConnectResult { 5 | /// `userId` is not null 6 | connectedAndAuthenticated, 7 | /// `userId` is null 8 | connectedButNotAuthenticated, 9 | notConnected, 10 | } 11 | 12 | abstract class AuthRepo { 13 | Future start({required void Function() onAutoReauthenticationFails}); 14 | int? get loggedUserId; 15 | bool isAuthenticated (); 16 | Future logout(); 17 | Future> authenticateWithEmailAndPassword({required String email, required String password}); 18 | void onAutoReauthenticationFails ({required void Function() onAutoReauthenticationFails}); 19 | void addOnLoggedInListener(void Function() listener); 20 | void addOnLogoutListener(void Function() listener); 21 | } 22 | -------------------------------------------------------------------------------- /flutter_app/lib/core/domain/repositories/connection_repo.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:askless/index.dart'; 3 | 4 | typedef OnDisconnectBecauseInvalidCredential = void Function(); 5 | 6 | abstract class ConnectionRepo { 7 | Stream streamConnectionChanges({bool immediately = false}); 8 | 9 | void start ({required void Function() onAutoReauthenticationFails}); 10 | } 11 | -------------------------------------------------------------------------------- /flutter_app/lib/core/domain/repositories/users_repo.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import '../entities/failures/failure.dart'; 3 | import '../../../features/chat/domain/entities/user_entity.dart'; 4 | 5 | 6 | abstract class UsersRepo { 7 | 8 | 9 | Future> createUser({ 10 | required String firstName, required String lastName, 11 | required String email, required String password, 12 | }); 13 | 14 | Stream> streamUsersToTalkStream(); 15 | 16 | Future> readUser(int userId); 17 | 18 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/domain/use_cases/initialize_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_chat_app_with_mysql/core/data/data_sources/hive_box_instance.dart'; 2 | import 'package:flutter_chat_app_with_mysql/core/domain/repositories/connection_repo.dart'; 3 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/repositories/messages_repo.dart'; 4 | import 'package:flutter_chat_app_with_mysql/core/domain/repositories/auth_repo.dart'; 5 | 6 | class InitializeApp { 7 | final HiveBoxInstance hiveBoxInstance; 8 | final MessagesRepo messagesRepo; 9 | final AuthRepo authRepo; 10 | final ConnectionRepo connectionRepo; 11 | bool _initialized = false; 12 | 13 | InitializeApp({required this.hiveBoxInstance, required this.connectionRepo, required this.messagesRepo, required this.authRepo}); 14 | 15 | Future start({required void Function() onAutoReauthenticationFails}) async { 16 | if (!_initialized) { 17 | _initialized = true; 18 | await hiveBoxInstance.initialize(); 19 | 20 | connectionRepo.start(onAutoReauthenticationFails: onAutoReauthenticationFails); 21 | authRepo.start(onAutoReauthenticationFails: onAutoReauthenticationFails); 22 | authRepo.addOnLoggedInListener(() { 23 | messagesRepo.start(); 24 | }); 25 | authRepo.addOnLogoutListener(() { 26 | messagesRepo.close(); 27 | }); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /flutter_app/lib/core/domain/use_cases/logout.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/repositories/messages_repo.dart'; 2 | import 'package:flutter_chat_app_with_mysql/core/domain/repositories/auth_repo.dart'; 3 | 4 | 5 | class Logout { 6 | final AuthRepo authRepository; 7 | final MessagesRepo messagesRepository; 8 | 9 | Logout({required this.authRepository, required this.messagesRepository,}); 10 | 11 | Future call () async { 12 | await authRepository.logout(); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/domain/use_cases/stream_connection_changes.dart: -------------------------------------------------------------------------------- 1 | import 'package:askless/index.dart'; 2 | import 'package:flutter_chat_app_with_mysql/core/domain/repositories/connection_repo.dart'; 3 | 4 | 5 | class StreamConnectionChanges { 6 | final ConnectionRepo connectionChangesRepo; 7 | 8 | StreamConnectionChanges({required this.connectionChangesRepo}); 9 | 10 | Stream call ({bool immediately = false}) { 11 | return connectionChangesRepo.streamConnectionChanges(immediately: immediately); 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/domain/use_cases/stream_users_to_talk.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import '../entities/failures/failure.dart'; 3 | import '../../../features/chat/domain/entities/user_entity.dart'; 4 | import '../repositories/users_repo.dart'; 5 | 6 | 7 | class UsersToTalkTo { 8 | final UsersRepo usersRepository; 9 | 10 | UsersToTalkTo({required this.usersRepository}); 11 | 12 | Stream> call() { 13 | return usersRepository.streamUsersToTalkStream(); 14 | } 15 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/utils/dartz_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | 3 | R asRight (Either either) { 4 | if(either.isLeft()){ 5 | throw "asRight failed, because is left"; 6 | } 7 | return (either as Right).value; 8 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/utils/formatted_text.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:askless/index.dart'; 3 | 4 | String formattedConnection(ConnectionStatus connection) => { 5 | ConnectionStatus.disconnected: "Disconnected", 6 | ConnectionStatus.connected: "Connected", 7 | ConnectionStatus.inProgress: "Connecting", 8 | }[connection] ?? "Unknown: $connection"; -------------------------------------------------------------------------------- /flutter_app/lib/core/utils/snackbar.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import 'package:flutter/material.dart'; 5 | 6 | void showSnackBarWarning ({required BuildContext context, required String message}) { 7 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( 8 | backgroundColor: Colors.indigo[900], 9 | duration: const Duration(seconds: 5), 10 | content: Row( 11 | children: [ 12 | const Icon(Icons.warning, size: 18, color: Colors.yellow,), 13 | const SizedBox(width: 8,), 14 | Expanded(child: Text(message, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600),)) 15 | ], 16 | ), 17 | )); 18 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/utils/validators.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | /// Adapted from https://stackoverflow.com/a/63292899/4508758 5 | String? validateEmail (String? value) { 6 | final requiredError = validateRequired(value); 7 | if(requiredError != null){ 8 | return requiredError; 9 | } 10 | 11 | const pattern = r"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'" 12 | r'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-' 13 | r'\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*' 14 | r'[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4]' 15 | r'[0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9]' 16 | r'[0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\' 17 | r'x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])'; 18 | final regex = RegExp(pattern); 19 | 20 | return !regex.hasMatch(value!) ? 'Invalid email address' : null; 21 | } 22 | 23 | String? validateRequired (String? value) { 24 | return value!.isNotEmpty == true ? null : 'This field is required'; 25 | } 26 | 27 | String? validateCreatePassword(String? value) { 28 | final requiredError = validateRequired(value); 29 | if(requiredError != null){ 30 | return requiredError; 31 | } 32 | if(value!.length < 6){ 33 | return "The password should contain at least 6 characters"; 34 | } 35 | return null; 36 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/widgets/button_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | 4 | class ButtonWidget extends StatelessWidget { 5 | final String text; 6 | final void Function()? onPressed; 7 | final bool isLoading; 8 | final bool isSmall; 9 | 10 | const ButtonWidget({Key? key, this.isSmall = false, required this.text, this.onPressed, this.isLoading = false}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return SizedBox( 15 | width: !isSmall ? double.infinity : null, 16 | height: isSmall ? 30 : 35, 17 | child: ElevatedButton( 18 | style: ButtonStyle( 19 | elevation: MaterialStateProperty.all(0), 20 | backgroundColor: MaterialStateProperty.all(Colors.blue[500]), 21 | shape: MaterialStateProperty.all( 22 | RoundedRectangleBorder( 23 | borderRadius: BorderRadius.circular(18.0), 24 | ) 25 | ) 26 | ), 27 | onPressed: isLoading ? null : onPressed, 28 | child: Padding( 29 | padding: EdgeInsets.symmetric(horizontal: 5), 30 | child: isLoading ? Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.blue[700]),),) : Text(text, style: TextStyle(fontSize: isSmall ? 12 : 14, letterSpacing: 2, color: Colors.white)), 31 | ), 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /flutter_app/lib/core/widgets/center_content_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:math' as math; 3 | 4 | import 'package:flutter_chat_app_with_mysql/main.dart'; 5 | 6 | /// Centers the `child` without a delimited container, 7 | /// so we are able to create animations outside this widget, 8 | /// like adding items to a list coming from the left or right 9 | class CenterContentWidget extends StatelessWidget { 10 | final Widget child; 11 | final bool withBackground; 12 | final EdgeInsets? padding; 13 | final double? verticalMargin; 14 | 15 | const CenterContentWidget({required this.child, this.verticalMargin, this.withBackground = false, Key? key, this.padding}) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Container( 20 | clipBehavior: Clip.none, 21 | decoration: !withBackground ? null : BoxDecoration( 22 | gradient: LinearGradient( 23 | colors: [ 24 | Colors.blue[900]!, 25 | Colors.blue[800]!, 26 | Colors.blue[900]!, 27 | ] 28 | ) 29 | ), 30 | child: Align( 31 | alignment: Alignment.topCenter, 32 | child: Builder( 33 | builder: (context) { 34 | return Padding( 35 | padding: padding ?? EdgeInsets.symmetric( 36 | vertical: verticalMargin ?? kMargin, 37 | horizontal: math.max(kMargin, (MediaQuery 38 | .of(context) 39 | .size 40 | .width - kPageContentWidth) / 2) 41 | ), 42 | child: SafeArea(child: child,), 43 | ); 44 | }) 45 | ) 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /flutter_app/lib/core/widgets/connection_status_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:askless/index.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_chat_app_with_mysql/core/domain/use_cases/stream_connection_changes.dart'; 4 | import '../../injection_container.dart'; 5 | import '../utils/formatted_text.dart'; 6 | 7 | 8 | class ConnectionStatusWidget extends StatelessWidget { 9 | const ConnectionStatusWidget({Key? key}) : super(key: key); 10 | 11 | Color getColor (ConnectionStatus connectionStatus){ 12 | if (connectionStatus == ConnectionStatus.connected) { 13 | return Colors.blue[900]!; 14 | } 15 | if (connectionStatus == ConnectionStatus.disconnected) { 16 | return Colors.red[300]!; 17 | } 18 | if (connectionStatus == ConnectionStatus.inProgress) { 19 | return Colors.grey[500]!; 20 | } 21 | throw "TODO: $connectionStatus"; 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return StreamBuilder( 27 | stream: getIt.get().call(immediately: true), 28 | builder: (context, snapshot) { 29 | if(!snapshot.hasData) { 30 | return Container(); 31 | } 32 | final status = snapshot.data!.status; 33 | 34 | return FittedBox( 35 | fit: BoxFit.scaleDown, 36 | child: Container( 37 | decoration: BoxDecoration( 38 | borderRadius: const BorderRadius.all(Radius.circular(10)), 39 | color: getColor(status) 40 | ), 41 | padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), 42 | child: Row( 43 | mainAxisSize: MainAxisSize.min, 44 | children: [ 45 | if(status == ConnectionStatus.connected) 46 | const Icon(Icons.link, size: 14, color: Colors.white), 47 | if(status == ConnectionStatus.inProgress) 48 | const Icon(Icons.wifi_protected_setup, size: 14, color: Colors.white), 49 | if(status == ConnectionStatus.disconnected) 50 | const Icon(Icons.link_off_outlined, size: 14, color: Colors.white), 51 | const SizedBox(width: 2,), 52 | Text(formattedConnection(status), style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 10, color: Colors.white)) 53 | ], 54 | ), 55 | ), 56 | ); 57 | }, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /flutter_app/lib/core/widgets/expanded_section_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Source: https://stackoverflow.com/a/54173729/4508758 4 | class ExpandedSection extends StatefulWidget { 5 | final Widget child; 6 | final bool expand; 7 | 8 | ExpandedSection({required this.expand, required this.child}); 9 | 10 | @override 11 | _ExpandedSectionState createState() => _ExpandedSectionState(); 12 | } 13 | 14 | class _ExpandedSectionState extends State with SingleTickerProviderStateMixin { 15 | late AnimationController expandController; 16 | late Animation animation; 17 | 18 | @override 19 | void initState() { 20 | super.initState(); 21 | prepareAnimations(); 22 | _runExpandCheck(); 23 | } 24 | 25 | ///Setting up the animation 26 | void prepareAnimations() { 27 | expandController = AnimationController( 28 | vsync: this, 29 | duration: Duration(milliseconds: 500) 30 | ); 31 | animation = CurvedAnimation( 32 | parent: expandController, 33 | curve: Curves.fastOutSlowIn, 34 | ); 35 | } 36 | 37 | void _runExpandCheck() { 38 | if(widget.expand) { 39 | expandController.forward(); 40 | } 41 | else { 42 | expandController.reverse(); 43 | } 44 | } 45 | 46 | @override 47 | void didUpdateWidget(ExpandedSection oldWidget) { 48 | super.didUpdateWidget(oldWidget); 49 | _runExpandCheck(); 50 | } 51 | 52 | @override 53 | void dispose() { 54 | expandController.dispose(); 55 | super.dispose(); 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return SizeTransition( 61 | axisAlignment: 1.0, 62 | sizeFactor: animation, 63 | child: widget.child 64 | ); 65 | } 66 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/widgets/my_appbar_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_chat_app_with_mysql/core/widgets/center_content_widget.dart'; 3 | import 'package:flutter_chat_app_with_mysql/main.dart'; 4 | import 'dart:math' as math; 5 | 6 | const double _height = 50; 7 | const double _leftIconSize = 28; 8 | const double _leftIconPaddingSide = 2; 9 | 10 | class MyAppBarWidget extends PreferredSize{ 11 | 12 | MyAppBarWidget({super.key, required BuildContext context, Widget? child, bool withBackground = true}) : super( 13 | preferredSize: const Size(double.infinity, _height), 14 | child: Container( 15 | color: Colors.blue[800]!, 16 | child: SafeArea( 17 | child: Container( 18 | decoration: !withBackground ? null : BoxDecoration( 19 | gradient: LinearGradient( 20 | colors: [ 21 | Colors.blue[900]!, 22 | Colors.blue[800]!, 23 | Colors.blue[900]!, 24 | ] 25 | ), 26 | boxShadow: [ 27 | BoxShadow(color: Colors.blue[900]!, offset: const Offset(0,0), spreadRadius: 2, blurRadius: 1) 28 | ] 29 | ), 30 | child: CenterContentWidget( 31 | verticalMargin: 0, 32 | child: Padding( 33 | padding: const EdgeInsets.only(left: 15, right: 15), 34 | child: Row( 35 | children: [ 36 | //on left 37 | FutureBuilder( 38 | future: Future.delayed(const Duration(milliseconds: 250)), 39 | builder: (context, _) { 40 | if(Navigator.of(context).canPop()) { 41 | return InkWell( 42 | child: Ink( 43 | child: const Icon(Icons.keyboard_arrow_left_rounded, color: Colors.white, size: _leftIconSize), 44 | ), 45 | onTap: () { 46 | Navigator.of(context).pop(); 47 | }, 48 | ); 49 | } 50 | return Container(); 51 | }, 52 | ), 53 | 54 | // on center 55 | Expanded( 56 | child: SizedBox( 57 | height: _height, 58 | child: Center( 59 | child: child, 60 | ), 61 | ), 62 | ), 63 | ], 64 | ), 65 | ) 66 | ) 67 | ), 68 | ), 69 | ) 70 | ); 71 | } 72 | 73 | -------------------------------------------------------------------------------- /flutter_app/lib/core/widgets/my_multiline_text_field.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_chat_app_with_mysql/main.dart'; 5 | 6 | class MyMultilineTextField extends StatelessWidget { 7 | final ValueChanged? onSubmitted; 8 | final Widget? prefixIcon; 9 | final Widget? suffixIcon; 10 | final TextEditingController controller; 11 | final String hintText; 12 | final Color? fillColor; 13 | final int maxLines; 14 | final int? maxLength; 15 | 16 | const MyMultilineTextField({Key? key, this.maxLength, this.fillColor, this.maxLines = 20, this.onSubmitted, required this.hintText, this.prefixIcon, this.suffixIcon, required this.controller,}) : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | const inputStyle = TextStyle(color: Colors.white, fontSize: 16); 21 | final border = OutlineInputBorder( 22 | borderRadius: const BorderRadius.all(Radius.circular(15)), 23 | borderSide: BorderSide(color: Colors.indigo[800]!, width: 0.0), 24 | ); 25 | 26 | return TextField( 27 | textInputAction: TextInputAction.go, 28 | controller: controller, 29 | onSubmitted: onSubmitted, 30 | keyboardType: TextInputType.multiline, 31 | minLines: 1, 32 | maxLines: maxLines, 33 | maxLength: maxLength, 34 | textAlignVertical: TextAlignVertical.center, 35 | clipBehavior: Clip.none, 36 | decoration: InputDecoration( 37 | prefixText: ' ', 38 | suffixIconConstraints: const BoxConstraints( 39 | maxHeight: kIconSize 40 | ), 41 | isCollapsed: true, 42 | contentPadding: const EdgeInsets.symmetric(vertical: 10,), 43 | isDense: true, 44 | hintStyle: TextStyle(color: Colors.blue[50]), 45 | hintText: hintText, 46 | fillColor: fillColor ?? Colors.indigo, 47 | focusedBorder: border, 48 | enabledBorder: border, 49 | errorBorder: border, 50 | disabledBorder: border, 51 | border: border, 52 | focusedErrorBorder: border, 53 | prefixIcon: prefixIcon, 54 | suffixIcon: suffixIcon, 55 | filled: true, 56 | ), 57 | style: inputStyle, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /flutter_app/lib/core/widgets/stopwatch/stopwatch_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | 4 | class StopwatchController { 5 | final List _listeners = []; 6 | bool _paused = true; 7 | late int _seconds; 8 | late int _stoppedSeconds; 9 | Duration? _reverseDuration; 10 | 11 | int get seconds { 12 | if (_reverseDuration != null) { 13 | return math.min(_stoppedSeconds, math.min(_seconds, _reverseDuration!.inSeconds)); 14 | } 15 | return math.max(_stoppedSeconds, math.max(_seconds, 0)); 16 | } 17 | 18 | StopwatchController({Duration? reverseDuration}) { 19 | _init(reverseDuration); 20 | } 21 | 22 | String get text { 23 | final minutesStr = seconds ~/ 60; 24 | final secondsStr = seconds % 60; 25 | return "${minutesStr < 10 ? '0' : ''}$minutesStr:${secondsStr < 10 ? '0' : ''}$secondsStr"; 26 | } 27 | 28 | Duration? get reverseDuration => _reverseDuration; 29 | 30 | void start({Duration? customReverseDuration}) { 31 | if (!_paused) { 32 | return; 33 | } 34 | _init(customReverseDuration); 35 | _paused = false; 36 | 37 | 38 | (() async { 39 | while(!_paused){ 40 | if (_reverseDuration != null) { 41 | _seconds--; 42 | } else { 43 | _seconds++; 44 | } 45 | for (final listener in _listeners) { 46 | listener(_seconds); 47 | } 48 | if (_seconds == 0 && _reverseDuration != null) { 49 | stop(); 50 | } else { 51 | await Future.delayed(const Duration(seconds: 1)); 52 | } 53 | } 54 | })(); 55 | } 56 | 57 | void addOnChangedListener ({required void Function(int seconds) listener}) { _listeners.add(listener); } 58 | void removeOnChangedListener ({required void Function(int seconds) listener}) { _listeners.remove(listener); } 59 | 60 | void dispose() { 61 | stop(); 62 | } 63 | void pause () { 64 | _paused = true; 65 | } 66 | void stop () { 67 | pause(); 68 | _stoppedSeconds = _seconds; 69 | if (_reverseDuration != null) { 70 | _seconds = _reverseDuration!.inSeconds + 1; 71 | } else { 72 | _seconds = -1; 73 | } 74 | } 75 | 76 | void _init(Duration? reverseDuration) { 77 | if (reverseDuration != null) { 78 | _reverseDuration = reverseDuration; 79 | } 80 | 81 | if (_reverseDuration != null) { 82 | _stoppedSeconds = _seconds = _reverseDuration!.inSeconds + 1; 83 | } else { 84 | _stoppedSeconds = _seconds = -1; 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /flutter_app/lib/core/widgets/stopwatch/stopwatch_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'stopwatch_controller.dart'; 4 | 5 | 6 | class StopwatchWidget extends StatefulWidget { 7 | final StopwatchController controller; 8 | final Color color; 9 | final double fontSize; 10 | 11 | const StopwatchWidget({this.color = Colors.white, required this.controller, this.fontSize = 22, super.key}); 12 | 13 | @override 14 | State createState() => _StopwatchWidgetState(); 15 | } 16 | 17 | class _StopwatchWidgetState extends State { 18 | @override 19 | void initState() { 20 | super.initState(); 21 | widget.controller.addOnChangedListener(listener: refresh); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Text(widget.controller.text, style: TextStyle(color: widget.color, fontWeight: FontWeight.w700, fontSize: widget.fontSize), ); 27 | } 28 | 29 | @override 30 | void dispose() { 31 | widget.controller.removeOnChangedListener(listener: refresh); 32 | super.dispose(); 33 | } 34 | 35 | void refresh(_) { 36 | setState(() {}); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /flutter_app/lib/core/widgets/waves_background/clipper/waves_background_clipper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Source: https://stackoverflow.com/a/61274521/4508758 4 | class WavesBackgroundClipper extends CustomClipper { 5 | @override 6 | Path getClip(Size size) { 7 | Path path = Path(); 8 | path.lineTo(0, size.height); 9 | path.quadraticBezierTo(size.width / 4, size.height - 40, size.width / 2, size.height - 20); 10 | path.quadraticBezierTo(3 / 4 * size.width, size.height, size.width, size.height - 30); 11 | path.lineTo(size.width, 0); 12 | 13 | return path; 14 | } 15 | 16 | @override 17 | bool shouldReclip(WavesBackgroundClipper oldClipper) => false; 18 | } 19 | -------------------------------------------------------------------------------- /flutter_app/lib/core/widgets/waves_background/waves_background.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'clipper/waves_background_clipper.dart'; 4 | 5 | 6 | class WavesBackground extends StatelessWidget { 7 | const WavesBackground({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Stack( 12 | children: [ 13 | Container( 14 | width: MediaQuery.of(context).size.width, 15 | height: MediaQuery.of(context).size.height, 16 | decoration: BoxDecoration( 17 | gradient: LinearGradient( 18 | colors: [ 19 | Colors.indigo[900]!, 20 | Colors.indigo[800]!, 21 | Colors.indigo[900]!, 22 | ] 23 | ) 24 | ), 25 | ), 26 | SizedBox( 27 | height: MediaQuery.of(context).size.height * .43, 28 | child: ClipPath( 29 | clipper: WavesBackgroundClipper(), 30 | child: Container( 31 | width: MediaQuery.of(context).size.width, 32 | height: MediaQuery.of(context).size.height, 33 | decoration: BoxDecoration( 34 | gradient: LinearGradient( 35 | colors: [ 36 | Colors.blue[900]!, 37 | Colors.blue[800]!, 38 | Colors.blue[900]!, 39 | ] 40 | ) 41 | ), 42 | ), 43 | ), 44 | ), 45 | ], 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /flutter_app/lib/environment.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | // TODO: replace with your own random text 4 | // Don't commit this file to your repository 5 | const String localStorageEncryptionKey = 'my unique and private encryption key (store securely and locally)'; -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/data/models/sending_text_message_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/entities/sending_text_message_entity.dart'; 2 | 3 | class SendingMessageModel extends SendingMessageEntity { 4 | /// Field names: 5 | static const String _kMessageId = "messageId"; 6 | static const String _kText = "text"; 7 | static const String _kReceiverUserId = "receiverUserId"; 8 | 9 | SendingMessageModel({required String messageId, required String text, required int receiverUserId}) : super(messageId: messageId, receiverUserId: receiverUserId, text: text,); 10 | 11 | SendingMessageModel.fromEntity(SendingMessageEntity entity) : super(messageId: entity.messageId, text: entity.text, receiverUserId: entity.receiverUserId); 12 | 13 | Map toMap () => { 14 | _kMessageId: messageId, 15 | _kText: text, 16 | _kReceiverUserId: receiverUserId, 17 | }; 18 | 19 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/data/models/sending_typing_model.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/entities/sending_typing_entity.dart'; 4 | 5 | class SendingTypingModel extends SendingTypingEntity { 6 | /// Field name: 7 | static const _kReceiverUserId = "receiverUserId"; 8 | 9 | SendingTypingModel({required int receiverUserId}) : super(receiverUserId: receiverUserId); 10 | 11 | SendingTypingModel.fromEntity(SendingTypingEntity entity) : super(receiverUserId: entity.receiverUserId); 12 | 13 | Map toMap () => { 14 | _kReceiverUserId: receiverUserId, 15 | }; 16 | 17 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/data/models/user_model.dart: -------------------------------------------------------------------------------- 1 | import '../../domain/entities/user_entity.dart'; 2 | 3 | 4 | class UserModel extends UserEntity { 5 | /// Field names: 6 | static const String _kUserId = "userId"; 7 | static const String _kFirstName = "firstName"; 8 | static const String _kLastName = "lastName"; 9 | 10 | UserModel({required int userId, required String firstName, required String lastName}) 11 | : super(userId: userId, firstName: firstName, lastName: lastName,); 12 | 13 | static UserModel fromMap(map) { 14 | return UserModel( 15 | userId: map[_kUserId], 16 | firstName: map[_kFirstName], 17 | lastName: map[_kLastName], 18 | ); 19 | } 20 | 21 | static List fromList(List list) { 22 | return list.map((data) => UserModel.fromMap(data)).toList(); 23 | } 24 | 25 | Map toMap() => { 26 | _kUserId: userId, 27 | _kFirstName: firstName, 28 | _kLastName: lastName 29 | }; 30 | 31 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/data/utils.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bool isDifferentDay (DateTime d1, DateTime d2) => DateTime(d1.year, d1.month, d1.day).difference(DateTime(d2.year, d2.month, d2.day)).inDays != 0; 5 | -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/entities/chat_content_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/entities/message_entity.dart'; 2 | 3 | 4 | class ChatContentEntity { 5 | final List messages; 6 | final bool isTyping; 7 | 8 | ChatContentEntity({required this.messages, required this.isTyping}); 9 | 10 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/entities/chat_list_item_entity.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/entities/message_entity.dart'; 6 | 7 | class ChatListItemEntity { 8 | 9 | } 10 | 11 | class SeparatorDateForMessages extends ChatListItemEntity { 12 | DateTime date; 13 | 14 | SeparatorDateForMessages({required this.date}); 15 | 16 | } 17 | 18 | class MessageChatListItemEntity extends ChatListItemEntity { 19 | final MessageEntity message; 20 | 21 | MessageChatListItemEntity({required this.message}); 22 | 23 | } 24 | 25 | class TypingIndicatorChatListItemEntity extends ChatListItemEntity { 26 | 27 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/entities/conversation_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/entities/message_entity.dart'; 2 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/entities/user_entity.dart'; 3 | 4 | class ConversationEntity { 5 | final UserEntity user; 6 | final MessageEntity? lastMessage; 7 | final bool isTyping; 8 | final int unreadMessagesAmount; 9 | 10 | ConversationEntity({required this.user, this.lastMessage, required this.isTyping, required this.unreadMessagesAmount,}); 11 | 12 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/entities/last_chat_messages_from_each_user_result.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import 'package:flutter_chat_app_with_mysql/features/chat/data/models/message_model.dart'; 5 | 6 | class LastChatMessagesFromEachUserResult { 7 | final List lastMessageList; 8 | final int unreadMessagesAmount; 9 | 10 | LastChatMessagesFromEachUserResult({required this.lastMessageList, required this.unreadMessagesAmount}); 11 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/entities/message_entity.dart: -------------------------------------------------------------------------------- 1 | 2 | enum SendStatus { 3 | pending, 4 | sendSuccessfully, 5 | sendFailed, 6 | } 7 | 8 | class MessageEntity { 9 | final String messageId; 10 | final String text; 11 | final DateTime? sentAt; 12 | final DateTime createdAt; 13 | DateTime? receivedAt; 14 | DateTime? readAt; 15 | SendStatus sendStatus; 16 | int senderUserId; 17 | int receiverUserId; 18 | 19 | // TIMESTAMPS DAS MENSAGENS ESTÃO MUDANDO PARECE, AGUARDE UM MINUTO E VEJA 20 | 21 | MessageEntity({ 22 | required this.messageId, 23 | required this.text, 24 | required this.senderUserId, 25 | required this.receiverUserId, 26 | required this.createdAt, 27 | this.sentAt, 28 | this.receivedAt, 29 | this.readAt, 30 | required this.sendStatus, 31 | }) { 32 | assert(text.isNotEmpty == true); 33 | } 34 | 35 | MessageEntity copyWith({SendStatus? sendStatus}) { 36 | return MessageEntity(messageId: messageId, text: text, senderUserId: senderUserId, receiverUserId: receiverUserId, createdAt: createdAt, sendStatus: sendStatus ?? this.sendStatus); 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/entities/model_source.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | enum ModelSource { 4 | localStorage, 5 | server, 6 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/entities/pending_request_to_api_telling_message_was_read.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | class PendingRequestToApiTellingMessagesWasRead { 4 | final int senderUserId; 5 | final String lastMessageId; 6 | 7 | PendingRequestToApiTellingMessagesWasRead({required this.lastMessageId, required this.senderUserId}); 8 | 9 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/entities/sending_text_message_entity.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | class SendingMessageEntity { 4 | final String messageId; 5 | final String text; 6 | final int receiverUserId; 7 | 8 | SendingMessageEntity({ 9 | required this.messageId, 10 | required this.text, 11 | required this.receiverUserId, 12 | }); 13 | 14 | 15 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/entities/sending_typing_entity.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | class SendingTypingEntity { 5 | final int receiverUserId; 6 | 7 | SendingTypingEntity({required this.receiverUserId}); 8 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/entities/user_entity.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | class UserEntity { 5 | final int userId; 6 | final String firstName; 7 | final String lastName; 8 | 9 | UserEntity({required this.userId, required this.firstName, required this.lastName}); 10 | 11 | String get fullName => "$firstName $lastName"; 12 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/repositories/messages_repo.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/entities/chat_content_entity.dart'; 3 | import 'package:flutter_chat_app_with_mysql/core/domain/entities/failures/failure.dart'; 4 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/entities/message_entity.dart'; 5 | import '../entities/conversation_entity.dart'; 6 | import '../entities/sending_text_message_entity.dart'; 7 | import '../entities/sending_typing_entity.dart'; 8 | 9 | 10 | 11 | abstract class MessagesRepo { 12 | 13 | Stream messagesStream({required int userId}); 14 | 15 | Future> notifyLoggedUserIsTyping({required SendingTypingEntity data}); 16 | 17 | Future> sendMessage({required SendingMessageEntity message}); 18 | 19 | Future> notifyLoggedUserReadConversation({required int userId, required String lastMessageId}); 20 | 21 | Stream> conversationsStream(); 22 | 23 | Future start(); 24 | Future close(); 25 | 26 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/use_cases/listen_to_conversations.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/repositories/messages_repo.dart'; 4 | 5 | import '../entities/conversation_entity.dart'; 6 | 7 | 8 | 9 | class ListenToConversationsWithMessages { 10 | final MessagesRepo messagesRepo; 11 | ListenToConversationsWithMessages({required this.messagesRepo}); 12 | 13 | Stream> call() { 14 | log("ListenToConversationsWithMessages called"); 15 | return messagesRepo.conversationsStream(); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/use_cases/messages_stream.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/entities/chat_content_entity.dart'; 2 | 3 | import '../repositories/messages_repo.dart'; 4 | 5 | 6 | class MessagesStream { 7 | final MessagesRepo messagesRepository; 8 | 9 | MessagesStream({required this.messagesRepository}); 10 | 11 | Stream call({required int userId}) { 12 | return messagesRepository.messagesStream(userId: userId); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/use_cases/notify_logged_user_is_typing.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_chat_app_with_mysql/core/domain/entities/failures/failure.dart'; 3 | import '../entities/sending_typing_entity.dart'; 4 | import '../repositories/messages_repo.dart'; 5 | 6 | 7 | class NotifyLoggedUserIsTyping { 8 | final MessagesRepo messagesRepository; 9 | 10 | NotifyLoggedUserIsTyping({required this.messagesRepository}); 11 | 12 | Future> call({required int receiverUserId}) { 13 | return messagesRepository.notifyLoggedUserIsTyping(data: SendingTypingEntity(receiverUserId: receiverUserId)); 14 | } 15 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/domain/use_cases/send_message.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_chat_app_with_mysql/core/domain/entities/failures/failure.dart'; 3 | import 'package:random_string/random_string.dart'; 4 | import '../entities/sending_text_message_entity.dart'; 5 | import '../repositories/messages_repo.dart'; 6 | 7 | 8 | class SendMessage { 9 | final MessagesRepo messagesRepository; 10 | 11 | SendMessage({required this.messagesRepository}); 12 | 13 | Future> call({required String text, required int receiverUserId}) { 14 | return messagesRepository.sendMessage(message: SendingMessageEntity(messageId: randomAlphaNumeric(28), text: text, receiverUserId: receiverUserId)); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/presentation/controllers/logout_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_chat_app_with_mysql/core/domain/use_cases/logout.dart'; 3 | import 'package:flutter_chat_app_with_mysql/screen_routes.dart'; 4 | import '../../../../injection_container.dart'; 5 | 6 | class LogoutController { 7 | 8 | void logout (BuildContext context) { 9 | Navigator.of(context).pushNamedAndRemoveUntil(ScreenRoutes.login, (route) => false); 10 | Future.delayed(const Duration(milliseconds: 500), getIt.get().call); 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/presentation/controllers/send_message_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/use_cases/notify_logged_user_is_typing.dart'; 4 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/use_cases/send_message.dart'; 5 | import 'package:flutter_chat_app_with_mysql/injection_container.dart'; 6 | 7 | class SendMessageController extends TextEditingController { 8 | final hasTextToSendNotifier = ValueNotifier(false); 9 | final showTextSentIconNotifier = ValueNotifier(false); 10 | final int receiverUserId; 11 | String _previousText = ""; 12 | 13 | SendMessageController({String? text, required this.receiverUserId}) : super(text: text) { 14 | addListener(() { 15 | if (_previousText != this.text && this.text.isNotEmpty) { 16 | getIt().call(receiverUserId: receiverUserId); 17 | } 18 | hasTextToSendNotifier.value = this.text.isNotEmpty; 19 | _previousText = this.text; 20 | }); 21 | } 22 | 23 | void sendMessage() { 24 | if (text.isEmpty) { 25 | log('No text to send'); 26 | return; 27 | } 28 | 29 | getIt.get().call(text: text, receiverUserId: receiverUserId); 30 | 31 | clear(); 32 | showTextSentIconNotifier.value = true; 33 | Future.delayed(const Duration(seconds: 1), () { 34 | showTextSentIconNotifier.value = false; 35 | }); 36 | } 37 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/presentation/controllers/users_to_talk_to_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_chat_app_with_mysql/core/domain/use_cases/stream_users_to_talk.dart'; 2 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/entities/user_entity.dart'; 3 | import 'package:flutter_chat_app_with_mysql/injection_container.dart'; 4 | 5 | 6 | class UsersToTalkToController { 7 | 8 | Stream> stream() { 9 | return getIt.get().call(); 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/presentation/widgets/chat_item_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/entities/chat_list_item_entity.dart'; 3 | import 'package:flutter_chat_app_with_mysql/features/chat/presentation/widgets/message_widget.dart'; 4 | import 'package:flutter_chat_app_with_mysql/features/chat/presentation/widgets/typing_indicator_widget.dart'; 5 | import 'package:flutter_chat_app_with_mysql/core/domain/repositories/auth_repo.dart'; 6 | import 'package:flutter_chat_app_with_mysql/injection_container.dart'; 7 | import 'separator_date_for_messages_widget.dart'; 8 | 9 | class ChatItemWidget extends StatelessWidget { 10 | final ChatListItemEntity chatItem; 11 | 12 | const ChatItemWidget ({required this.chatItem, Key? key}) : super(key: key); 13 | 14 | int get loggedUserId => getIt.get().loggedUserId!; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | if (chatItem is SeparatorDateForMessages) { 19 | return SeparatorDateForMessagesWidget( 20 | dateTime: (chatItem as SeparatorDateForMessages).date, 21 | ); 22 | } 23 | if (chatItem is MessageChatListItemEntity) { 24 | return MessageSideWidget(message: (chatItem as MessageChatListItemEntity).message, key: ValueKey((chatItem as MessageChatListItemEntity).message.messageId),); 25 | } 26 | if (chatItem is TypingIndicatorChatListItemEntity) { 27 | return const TypingIndicatorWidget(margin: EdgeInsets.only(top: 7),); 28 | } 29 | throw "TODO: ${chatItem.toString()}"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/presentation/widgets/delay_animate_switcher.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:math' as math; 3 | 4 | class DelayAnimateSwitcher extends StatefulWidget { 5 | final bool animate; 6 | final Duration delay; 7 | final Duration animationDuration; 8 | final Widget secondChild; 9 | final Widget? firstChild; 10 | final Widget Function(Widget child, Animation animation)? transitionBuilder; 11 | 12 | const DelayAnimateSwitcher({Key? key, 13 | required this.secondChild, 14 | this.firstChild, 15 | this.animate = true, 16 | this.delay = const Duration(milliseconds: 1), 17 | this.transitionBuilder, 18 | this.animationDuration = const Duration(milliseconds: 150)}) : super(key: key); 19 | 20 | @override 21 | State createState() => _DelayAnimateSwitcherState(); 22 | } 23 | 24 | class _DelayAnimateSwitcherState extends State { 25 | bool showChild2 = false; 26 | bool disposed = false; 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | 32 | if (widget.animate && widget.animationDuration.inMilliseconds > 0) { 33 | Future.delayed( 34 | Duration(milliseconds: math.max(widget.delay.inMilliseconds, 10))) 35 | .then((_) { 36 | if (!disposed) { 37 | setState(() { 38 | showChild2 = true; 39 | }); 40 | } 41 | }); 42 | } else { 43 | showChild2 = true; 44 | } 45 | } 46 | 47 | 48 | @override 49 | void dispose() { 50 | disposed = true; 51 | super.dispose(); 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | if (!widget.animate || widget.animationDuration.inMilliseconds == 0) { 57 | return widget.secondChild; 58 | } 59 | 60 | return AnimatedSwitcher( 61 | duration: widget.animationDuration, 62 | transitionBuilder: widget.transitionBuilder ?? (Widget child, Animation animation) { 63 | return ScaleTransition(scale: animation, child: child); 64 | }, 65 | child: showChild2 66 | ? widget.secondChild 67 | : (widget.firstChild ?? Container()), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/presentation/widgets/load_more_messages_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class LoadMoreMessagesButton extends StatelessWidget { 5 | final void Function() onTap; 6 | 7 | const LoadMoreMessagesButton({required this.onTap, Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Align( 12 | alignment: Alignment.center, 13 | child: InkWell( 14 | onTap: onTap, 15 | child: Ink( 16 | child: Container( 17 | margin: const EdgeInsets.only(top: 17, bottom: 12), 18 | padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 5), 19 | decoration: BoxDecoration( 20 | color: Colors.lightBlue[50], 21 | borderRadius: const BorderRadius.all(Radius.circular(50)), 22 | ), 23 | child: const Text("Load more messages", style: TextStyle(color: Colors.indigo, fontSize: 14)), 24 | ), 25 | ), 26 | ) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/presentation/widgets/logout_button_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_chat_app_with_mysql/features/chat/presentation/controllers/logout_controller.dart'; 3 | 4 | class LogoutButtonWidget extends StatelessWidget { 5 | final logoutController = LogoutController(); 6 | 7 | LogoutButtonWidget({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return InkWell( 12 | onTap: () { 13 | logoutController.logout(context); 14 | }, 15 | child: Ink( 16 | child: Container( 17 | decoration: BoxDecoration( 18 | color: Colors.blue[800], 19 | borderRadius: BorderRadius.circular(10) 20 | ), 21 | padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), 22 | child: Icon(Icons.logout_outlined, color: Colors.blue[50]!,), 23 | ), 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/presentation/widgets/message_status_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/entities/message_entity.dart'; 3 | import 'package:flutter_chat_app_with_mysql/core/domain/repositories/auth_repo.dart'; 4 | import '../../../../injection_container.dart'; 5 | import 'delay_animate_switcher.dart'; 6 | 7 | class MessageStatusWidget extends StatelessWidget { 8 | final MessageEntity message; 9 | get checkIcon => Icon(Icons.check, size: 16, color: message.readAt != null ? Colors.blue[300] : Colors.green[50]); 10 | 11 | int get loggedUserId => getIt.get().loggedUserId!; 12 | bool get isLeftSide => message.senderUserId != loggedUserId; 13 | 14 | const MessageStatusWidget({Key? key, required this.message}) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Row( 19 | mainAxisSize: MainAxisSize.min, 20 | crossAxisAlignment: CrossAxisAlignment.end, 21 | children: [ 22 | if (!isLeftSide && message.sendStatus == SendStatus.pending) 23 | Icon(Icons.access_time_outlined, color: Colors.green[50], size: 17), 24 | if (!isLeftSide && message.sendStatus == SendStatus.sendFailed) 25 | Icon(Icons.error_outline_rounded, color: Colors.red[300], size: 17), 26 | if(!isLeftSide && (message.sentAt != null || message.receivedAt != null || message.readAt != null)) 27 | SizedBox( 28 | width: message.receivedAt != null || message.readAt != null ? 25 : null, 29 | child: Stack( 30 | children: [ 31 | DelayAnimateSwitcher( 32 | firstChild: Container(width: 18,), 33 | secondChild: checkIcon, 34 | animate: message.receivedAt == null ? false : (DateTime.now().millisecondsSinceEpoch - 1000 < message.receivedAt!.millisecondsSinceEpoch), 35 | ), 36 | if (message.receivedAt != null || message.readAt != null) 37 | Align( 38 | alignment: const Alignment(.85,0), 39 | child: DelayAnimateSwitcher( 40 | firstChild: Container(width: 18,), 41 | secondChild: checkIcon, 42 | animate: DateTime.now().millisecondsSinceEpoch - 1000 < message.sentAt!.millisecondsSinceEpoch, 43 | delay: const Duration(milliseconds: 320) 44 | ), 45 | ) 46 | ], 47 | ), 48 | ) 49 | ], 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/presentation/widgets/separator_date_for_messages_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class SeparatorDateForMessagesWidget extends StatelessWidget { 5 | DateTime dateTime; 6 | 7 | SeparatorDateForMessagesWidget({required this.dateTime, Key? key}) : super(key: key); 8 | 9 | String get text { 10 | DateTime now = DateTime.now(); 11 | final int differenceInDays = DateTime(dateTime.year, dateTime.month, dateTime.day).difference(DateTime(now.year, now.month, now.day)).inDays; 12 | if(differenceInDays == 0){ 13 | return 'Today'; 14 | } 15 | if(differenceInDays == -1){ 16 | return 'Yesterday'; 17 | } 18 | if(differenceInDays > -7){ 19 | return [ 20 | 'Monday', 21 | 'Tuesday', 22 | 'Wednesday', 23 | 'Thursday', 24 | 'Friday', 25 | 'Saturday', 26 | 'Sunday', 27 | ][dateTime.weekday-1]; 28 | } 29 | if(differenceInDays > -365){ 30 | return '${[ 31 | 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' 32 | ][dateTime.month-1]} ${dateTime.day}'; 33 | } 34 | return DateFormat('yyyy/MM/dd').format(dateTime); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return Align( 40 | alignment: Alignment.center, 41 | child: Container( 42 | margin: const EdgeInsets.only(top: 17, bottom: 12), 43 | padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 5), 44 | decoration: BoxDecoration( 45 | color: Colors.lightBlue[50], 46 | borderRadius: const BorderRadius.all(Radius.circular(50)), 47 | ), 48 | child: Text(text, style: const TextStyle(color: Colors.indigo, fontSize: 14)), 49 | ) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /flutter_app/lib/features/chat/presentation/widgets/typing_indicator_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_chat_app_with_mysql/features/chat/presentation/widgets/balloon_widget.dart'; 4 | 5 | import 'message_widget.dart'; 6 | 7 | class _DotWidget extends StatefulWidget { 8 | Duration delayToStart; 9 | 10 | _DotWidget({Key? key, required this.delayToStart}) : super(key: key); 11 | 12 | @override 13 | State<_DotWidget> createState() => _DotWidgetState(); 14 | } 15 | 16 | class _DotWidgetState extends State<_DotWidget> { 17 | final Duration duration = const Duration(milliseconds: 350); 18 | bool running = true; 19 | final double circleSize = 4.0; 20 | final double totalHeight = 10; 21 | bool isBottom = true; 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | 27 | late void Function() func; 28 | func = () => Future.delayed(duration, () { 29 | if(running){ 30 | setState(() { 31 | isBottom = !isBottom; 32 | Future.delayed(isBottom ? const Duration(milliseconds: 800) : Duration.zero, func); 33 | }); 34 | } 35 | }); 36 | Future.delayed(widget.delayToStart, func); 37 | } 38 | 39 | @override 40 | void dispose() { 41 | running = false; 42 | super.dispose(); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return SizedBox( 48 | height: totalHeight, 49 | width: circleSize, 50 | child: Stack( 51 | children: [ 52 | AnimatedPositioned( 53 | bottom: isBottom ? 0 : (totalHeight - circleSize), 54 | duration: duration, 55 | child: Container( 56 | decoration: BoxDecoration( 57 | color: Colors.indigo, 58 | borderRadius: BorderRadius.circular(50), 59 | ), 60 | width: circleSize, 61 | height: circleSize, 62 | ), 63 | ), 64 | ], 65 | ), 66 | ); 67 | } 68 | } 69 | 70 | class TypingIndicatorWidget extends StatelessWidget { 71 | final EdgeInsets margin; 72 | const TypingIndicatorWidget({Key? key, this.margin = EdgeInsets.zero}) : super(key: key); 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return Padding( 77 | padding: margin, 78 | child: BalloonWidget( 79 | isLeftSide: true, 80 | centerChild: Padding( 81 | padding: const EdgeInsets.only(bottom: 8, top: 2, left: 8, right: 8), 82 | child: Row( 83 | mainAxisSize: MainAxisSize.min, 84 | children: [ 85 | _DotWidget(delayToStart: Duration.zero,), 86 | const SizedBox(width: 4,), 87 | _DotWidget(delayToStart: const Duration(milliseconds: 250),), 88 | const SizedBox(width: 4,), 89 | _DotWidget(delayToStart: const Duration(milliseconds: 500),), 90 | ], 91 | ), 92 | ), 93 | ), 94 | ); 95 | } 96 | } 97 | 98 | -------------------------------------------------------------------------------- /flutter_app/lib/features/login_and_registration/data/data_sources/auth_remote_ds.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | import 'package:askless/index.dart'; 3 | import 'package:flutter_chat_app_with_mysql/features/login_and_registration/domain/entities/failures/credential_failure.dart'; 4 | import 'package:flutter_chat_app_with_mysql/features/login_and_registration/domain/entities/failures/invalid_refresh_token_failure.dart'; 5 | import 'package:flutter_chat_app_with_mysql/features/login_and_registration/domain/entities/tokens_entity.dart'; 6 | import '../../../../core/domain/entities/failures/failure.dart'; 7 | import '../../../../main.dart'; 8 | import '../../domain/entities/failures/invalid_email_failure.dart'; 9 | import '../../domain/entities/failures/invalid_password_failure.dart'; 10 | import '../models/tokens_model.dart'; 11 | 12 | class AuthRemoteDS { 13 | 14 | 15 | Future logout () async { 16 | await AsklessClient.instance.create(route: "logout", body: {}); 17 | AsklessClient.instance.clearAuthentication(); 18 | } 19 | 20 | Future getAccessTokenWithEmailAndPassword ({required String email, required String password}) async { 21 | final res = (await AsklessClient.instance.create(route: "login", body: {"email": email, "password": password})); 22 | log("connectWithEmailAndPassword, result is ${res.success ? "success" : "error"}"); 23 | if (!res.success) { 24 | log("connectWithEmailAndPassword error with code ${res.error!.code}: ${res.error!.description}"); 25 | if (res.error!.code == "INVALID_EMAIL") { throw InvalidEmailFailure(); } 26 | if (res.error!.code == "INVALID_PASSWORD"){ throw InvalidPasswordFailure(); } 27 | throw Failure(); 28 | } 29 | return TokensModel.fromMap(res.output); 30 | } 31 | 32 | /// throws [CredentialFailure] 33 | Future authenticateWithAccessToken ({required String accessToken, bool neverTimeout = false}) async { 34 | final res = (await AsklessClient.instance.authenticate(credential: { "accessToken": accessToken }, neverTimeout: neverTimeout)); 35 | log ("AUTHENTICATED: ${res.success}"); 36 | if (!res.success) { 37 | log("connectWithAccessToken error: ${res.error!.code}"); 38 | // if (res.errorCode == "EXPIRED_ACCESS_TOKEN") { // <-- Another option 39 | if (res.error!.isCredentialError) { 40 | throw CredentialFailure(credentialErrorCode: res.error!.code); 41 | } 42 | throw Failure(); 43 | } 44 | } 45 | 46 | Future useRefreshTokenToGetNewAccessToken({required int userId, required String refreshToken}) async { 47 | final res = await AsklessClient.instance.create(route: "accessToken", body: { 48 | "refreshToken": refreshToken, 49 | "userId": userId, 50 | }, neverTimeout: false); 51 | if (res.success) { 52 | return TokensModel.fromMap(res.output); 53 | } 54 | if (res.error!.code == "INVALID_REFRESH_TOKEN") { 55 | throw InvalidRefreshTokenFailure(); 56 | } 57 | log("Unknown error when trying to refresh the token: ${res.error!.code}"); 58 | throw Failure(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /flutter_app/lib/features/login_and_registration/data/models/tokens_model.dart: -------------------------------------------------------------------------------- 1 | import '../../domain/entities/tokens_entity.dart'; 2 | 3 | 4 | 5 | class TokensModel extends TokensEntity { 6 | static const _kAccessToken = "accessToken"; 7 | static const _kRefreshToken = "refreshToken"; 8 | static const _kUserId = "userId"; 9 | static const _kAccessTokenExpirationMsSinceEpoch = "accessTokenExpirationMsSinceEpoch"; 10 | 11 | TokensModel({ 12 | required String accessToken, 13 | required String refreshToken, 14 | required int loggedUserId, 15 | required DateTime accessTokenExpiration 16 | }) : super ( 17 | userId: loggedUserId, 18 | accessToken: accessToken, 19 | refreshToken: refreshToken, 20 | accessTokenExpiration: accessTokenExpiration, 21 | ); 22 | 23 | static TokensModel fromMap (output) { 24 | return TokensModel( 25 | accessToken: output[_kAccessToken], 26 | refreshToken: output[_kRefreshToken], 27 | loggedUserId: output[_kUserId], 28 | accessTokenExpiration: DateTime.fromMillisecondsSinceEpoch(output[_kAccessTokenExpirationMsSinceEpoch]) 29 | ); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/login_and_registration/domain/entities/failures/credential_failure.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:flutter_chat_app_with_mysql/core/domain/entities/failures/failure.dart'; 4 | 5 | class CredentialFailure extends Failure { 6 | final String credentialErrorCode; 7 | 8 | CredentialFailure({required this.credentialErrorCode}); 9 | 10 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/login_and_registration/domain/entities/failures/email_already_exists_failure.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_chat_app_with_mysql/core/domain/entities/failures/failure.dart'; 2 | 3 | 4 | class EmailAlreadyExistsFailure extends Failure { 5 | 6 | EmailAlreadyExistsFailure() : super ("Email is already in use, please, try to login into your account"); 7 | 8 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/login_and_registration/domain/entities/failures/invalid_email_failure.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:flutter_chat_app_with_mysql/core/domain/entities/failures/failure.dart'; 4 | 5 | class InvalidEmailFailure extends Failure { 6 | 7 | InvalidEmailFailure() : super("Oops! Looks like this is an invalid email"); 8 | 9 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/login_and_registration/domain/entities/failures/invalid_password_failure.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import '../../../../../core/domain/entities/failures/failure.dart'; 5 | 6 | class InvalidPasswordFailure extends Failure { 7 | 8 | InvalidPasswordFailure() : super("Oops! This is not the correct password"); 9 | 10 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/login_and_registration/domain/entities/failures/invalid_refresh_token_failure.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:flutter_chat_app_with_mysql/core/domain/entities/failures/failure.dart'; 4 | 5 | class InvalidRefreshTokenFailure extends Failure {} -------------------------------------------------------------------------------- /flutter_app/lib/features/login_and_registration/domain/entities/tokens_entity.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | class TokensEntity { 5 | final String accessToken; 6 | final String refreshToken; 7 | final int userId; 8 | final DateTime accessTokenExpiration; 9 | 10 | TokensEntity({ 11 | required this.accessToken, 12 | required this.refreshToken, 13 | required this.userId, 14 | required this.accessTokenExpiration 15 | }); 16 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/login_and_registration/domain/use_cases/login.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_chat_app_with_mysql/core/domain/entities/failures/failure.dart'; 3 | import 'package:flutter_chat_app_with_mysql/features/chat/domain/repositories/messages_repo.dart'; 4 | import '../../../../core/domain/repositories/auth_repo.dart'; 5 | 6 | 7 | class Login { 8 | final AuthRepo authRepository; 9 | final MessagesRepo messagesRepository; 10 | 11 | Login({required this.authRepository, required this.messagesRepository}); 12 | 13 | Future> call ({required String email, required String password}) async { 14 | return authRepository.authenticateWithEmailAndPassword(email: email, password: password); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/login_and_registration/domain/use_cases/register.dart: -------------------------------------------------------------------------------- 1 | import 'package:dartz/dartz.dart'; 2 | import 'package:flutter_chat_app_with_mysql/core/domain/repositories/users_repo.dart'; 3 | import '../../../../core/domain/entities/failures/failure.dart'; 4 | import '../../../chat/domain/entities/user_entity.dart'; 5 | 6 | 7 | class Register { 8 | 9 | final UsersRepo usersRepository; 10 | 11 | Register({required this.usersRepository}); 12 | 13 | Future> call ({ 14 | required String firstName, required String lastName, 15 | required String email, required String password, 16 | }) { 17 | return usersRepository.createUser(firstName: firstName, lastName: lastName, email: email, password: password,); 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /flutter_app/lib/features/login_and_registration/screens/widgets/icon/animated_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MyAnimatedIcon extends StatefulWidget { 4 | final IconData icon; 5 | late final ValueNotifier? notifySuccess; 6 | late final ValueNotifier? notifyError; 7 | 8 | MyAnimatedIcon( 9 | { required this.icon, 10 | this.notifySuccess, 11 | this.notifyError, 12 | Key? key} 13 | ) : super(key: key); 14 | 15 | @override 16 | State createState() => _MyAnimatedIconState(); 17 | } 18 | 19 | class _MyAnimatedIconState extends State { 20 | ValueNotifier? internalNotifySuccess; 21 | ValueNotifier? internalNotifyError; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return ValueListenableBuilder( 26 | valueListenable: (widget.notifySuccess ?? internalNotifySuccess)!, 27 | builder: (context, showSuccess, _) { 28 | return ValueListenableBuilder( 29 | valueListenable: (widget.notifyError ?? internalNotifyError)!, 30 | builder: (context, showError, __) { 31 | return AnimatedSwitcher( 32 | duration: const Duration(milliseconds: 300), 33 | transitionBuilder: (Widget child, Animation animation) { 34 | return ScaleTransition(scale: animation, child: child); 35 | }, 36 | child: () { 37 | const double kIconSize = 27.0; 38 | if (showSuccess) { 39 | return const Icon( 40 | key: ValueKey(0), 41 | Icons.check, 42 | color: Colors.indigo, 43 | size: kIconSize, 44 | ); 45 | } 46 | if (showError?.isNotEmpty == true) { 47 | return Icon( 48 | key: ValueKey(1), 49 | Icons.error_outline_rounded, 50 | color: Colors.red[300]!, 51 | size: kIconSize, 52 | ); 53 | } 54 | return Icon( 55 | key: const ValueKey(2), 56 | widget.icon, 57 | color: Colors.indigo, 58 | size: kIconSize, 59 | ); 60 | }(), 61 | ); 62 | } 63 | ); 64 | } 65 | ); 66 | } 67 | 68 | @override 69 | void initState() { 70 | super.initState(); 71 | 72 | if (widget.notifySuccess == null) { 73 | internalNotifySuccess = ValueNotifier(false); 74 | } 75 | if (widget.notifyError == null) { 76 | internalNotifyError = ValueNotifier(null); 77 | } 78 | } 79 | 80 | @override 81 | void dispose() { 82 | internalNotifySuccess?.dispose(); 83 | internalNotifyError?.dispose(); 84 | super.dispose(); 85 | } 86 | } -------------------------------------------------------------------------------- /flutter_app/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_chat_app_with_mysql/screen_routes.dart'; 3 | import 'injection_container.dart' as injection_container; 4 | 5 | 6 | void main() { 7 | WidgetsFlutterBinding.ensureInitialized(); 8 | injection_container.init(); 9 | runApp(const MyApp()); 10 | } 11 | 12 | const double kMargin = 16.0; 13 | const double kPageContentWidth = 600; 14 | const double kIconSize = 22.0; 15 | 16 | final navigatorKey = GlobalKey(); 17 | class MyApp extends StatelessWidget { 18 | 19 | const MyApp({super.key}); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return MaterialApp( 24 | title: 'Flutter with Mysql', 25 | debugShowCheckedModeBanner: false, 26 | initialRoute: ScreenRoutes.loading, /// Check this file to see how the App starts: lib/features/loading/screens/loading_screen.dart 27 | theme: ThemeData( 28 | primarySwatch: Colors.indigo, 29 | fontFamily: 'RedHatDisplay', 30 | ), 31 | routes: screenRoutes, 32 | navigatorKey: navigatorKey, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /flutter_app/lib/screen_routes.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'features/call/presentation/screens/call_screen.dart'; 3 | import 'features/chat/presentation/screens/realtime_chat_screen/realtime_chat_screen.dart'; 4 | import 'features/chat/presentation/screens/realtime_conversations_screen/realtime_conversations_screen.dart'; 5 | import 'features/loading/screens/loading_screen.dart'; 6 | import 'features/login_and_registration/screens/login_and_registration_screen.dart'; 7 | 8 | class ScreenRoutes { 9 | /// home route 10 | static const loading = LoadingScreen.route; 11 | static const login = LoginAndRegistrationScreen.route; 12 | static const conversations = RealtimeConversationsScreen.route; 13 | static const chat = RealtimeChatScreen.route; 14 | static const requestCall = CallScreen.route; 15 | } 16 | 17 | Map screenRoutes = { 18 | ScreenRoutes.loading: (context) => const LoadingScreen(), 19 | ScreenRoutes.login: (context) => const LoginAndRegistrationScreen(), 20 | ScreenRoutes.chat: (context) => const RealtimeChatScreen(), 21 | ScreenRoutes.conversations: (context) => const RealtimeConversationsScreen(), 22 | ScreenRoutes.requestCall: (context) => const CallScreen(), 23 | }; -------------------------------------------------------------------------------- /flutter_app/linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /flutter_app/linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | 11 | void fl_register_plugins(FlPluginRegistry* registry) { 12 | g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = 13 | fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); 14 | flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); 15 | } 16 | -------------------------------------------------------------------------------- /flutter_app/linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /flutter_app/linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | flutter_webrtc 7 | ) 8 | 9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 10 | ) 11 | 12 | set(PLUGIN_BUNDLED_LIBRARIES) 13 | 14 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 19 | endforeach(plugin) 20 | 21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 24 | endforeach(ffi_plugin) 25 | -------------------------------------------------------------------------------- /flutter_app/linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /flutter_app/linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /flutter_app/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /flutter_app/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /flutter_app/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import connectivity_plus 9 | import flutter_webrtc 10 | import path_provider_foundation 11 | 12 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 13 | ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) 14 | FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) 15 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 16 | } 17 | -------------------------------------------------------------------------------- /flutter_app/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = flutter_chat_app_with_mysql 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterchatappwithmysql.flutterChatAppWithMysql 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.example.flutter_chat_app_with_mysql. All rights reserved. 15 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /flutter_app/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /flutter_app/macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /flutter_app/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_chat_app_with_mysql 2 | description: A Flutter Chat App with Node.js, WebSockets and MySQL 3 | 4 | publish_to: 'none' 5 | version: 1.0.0+1 6 | environment: 7 | sdk: '>=3.0.5 <4.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | cupertino_icons: ^1.0.6 13 | intl: ^0.18.1 14 | dartz: ^0.10.1 15 | get_it: ^7.6.4 16 | crypto: ^3.0.3 17 | hive_flutter: ^1.1.0 18 | random_string: ^2.3.1 19 | flutter_keyboard_visibility: ^5.4.1 20 | askless: ^3.1.1 21 | 22 | dev_dependencies: 23 | flutter_test: 24 | sdk: flutter 25 | flutter_lints: ^2.0.3 26 | 27 | flutter: 28 | uses-material-design: true 29 | 30 | assets: 31 | - assets/ 32 | - assets/fonts/RedHatDisplay/ 33 | 34 | fonts: 35 | - family: RedHatDisplay 36 | fonts: 37 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-Light.ttf 38 | weight: 100 39 | style: normal 40 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-LightItalic.ttf 41 | weight: 100 42 | style: italic 43 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-Light.ttf 44 | weight: 200 45 | style: normal 46 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-LightItalic.ttf 47 | weight: 200 48 | style: italic 49 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-Regular.ttf 50 | weight: 300 51 | style: normal 52 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-Italic.ttf 53 | weight: 300 54 | style: normal 55 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-Medium.ttf 56 | weight: 400 57 | style: normal 58 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-MediumItalic.ttf 59 | weight: 400 60 | style: italic 61 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-SemiBold.ttf 62 | weight: 500 63 | style: normal 64 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-SemiBoldItalic.ttf 65 | weight: 500 66 | style: italic 67 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-Bold.ttf 68 | weight: 600 69 | style: normal 70 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-BoldItalic.ttf 71 | weight: 600 72 | style: italic 73 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-ExtraBold.ttf 74 | weight: 700 75 | style: normal 76 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-ExtraBoldItalic.ttf 77 | weight: 700 78 | style: italic 79 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-BlackItalic.ttf 80 | weight: 800 81 | style: italic 82 | - asset: assets/fonts/RedHatDisplay/RedHatDisplay-BlackItalic.ttf 83 | weight: 800 84 | style: italic 85 | -------------------------------------------------------------------------------- /flutter_app/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/web/favicon.png -------------------------------------------------------------------------------- /flutter_app/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/web/icons/Icon-192.png -------------------------------------------------------------------------------- /flutter_app/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/web/icons/Icon-512.png -------------------------------------------------------------------------------- /flutter_app/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /flutter_app/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /flutter_app/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | flutter_chat_app_with_mysql 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /flutter_app/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter_chat_app_with_mysql", 3 | "short_name": "flutter_chat_app_with_mysql", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A flutter chat app with mysql, without firebase and built with websocket", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /flutter_app/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /flutter_app/windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | 12 | void RegisterPlugins(flutter::PluginRegistry* registry) { 13 | ConnectivityPlusWindowsPluginRegisterWithRegistrar( 14 | registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); 15 | FlutterWebRTCPluginRegisterWithRegistrar( 16 | registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); 17 | } 18 | -------------------------------------------------------------------------------- /flutter_app/windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /flutter_app/windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | connectivity_plus 7 | flutter_webrtc 8 | ) 9 | 10 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 11 | ) 12 | 13 | set(PLUGIN_BUNDLED_LIBRARIES) 14 | 15 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 16 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 17 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 20 | endforeach(plugin) 21 | 22 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 23 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 24 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 25 | endforeach(ffi_plugin) 26 | -------------------------------------------------------------------------------- /flutter_app/windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /flutter_app/windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | return true; 35 | } 36 | 37 | void FlutterWindow::OnDestroy() { 38 | if (flutter_controller_) { 39 | flutter_controller_ = nullptr; 40 | } 41 | 42 | Win32Window::OnDestroy(); 43 | } 44 | 45 | LRESULT 46 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 47 | WPARAM const wparam, 48 | LPARAM const lparam) noexcept { 49 | // Give Flutter, including plugins, an opportunity to handle window messages. 50 | if (flutter_controller_) { 51 | std::optional result = 52 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 53 | lparam); 54 | if (result) { 55 | return *result; 56 | } 57 | } 58 | 59 | switch (message) { 60 | case WM_FONTCHANGE: 61 | flutter_controller_->engine()->ReloadSystemFonts(); 62 | break; 63 | } 64 | 65 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 66 | } 67 | -------------------------------------------------------------------------------- /flutter_app/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /flutter_app/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"flutter_chat_app_with_mysql", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /flutter_app/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /flutter_app/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoBertotti/flutter_chat_app_with_nodejs/af42432f69195b044c5d38c666a578bbccb5de85/flutter_app/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /flutter_app/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /flutter_app/windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length <= 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /flutter_app/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/.gitignore: -------------------------------------------------------------------------------- 1 | src/environment/jwt-private.key 2 | src/environment/db.ts 3 | .idea/ 4 | .vscode/ 5 | node_modules/ 6 | build/ 7 | tmp/ 8 | temp/ 9 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat_with_mysql_backend", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "tsc && cpx \"src/environment/**\" \"build/environment\"", 6 | "dev": "nodemon --exec ts-node src/index.ts", 7 | "start": "ts-node src/index.ts" 8 | }, 9 | "dependencies": { 10 | "bcrypt": "^5.1.1", 11 | "bufferutil": "^4.0.7", 12 | "jsonwebtoken": "^9.0.2", 13 | "mysql": "^2.18.1", 14 | "reflect-metadata": "^0.1.13", 15 | "typeorm": "0.3.17", 16 | "utf-8-validate": "^6.0.3", 17 | "askless": "^2.0.4" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20.5.9", 21 | "cpx": "^1.5.0", 22 | "nodemon": "^3.0.1", 23 | "ts-node": "10.9.1", 24 | "typescript": "5.2.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/controllers/auth-controller/auth-controller.ts: -------------------------------------------------------------------------------- 1 | import {AuthService} from "../../domain/services/auth-service"; 2 | import {authService, Controller} from "../../domain/controllers-and-services"; 3 | import {TokensModel} from "../../data/models/tokens-model"; 4 | import {AsklessServer, Authenticate} from "askless"; 5 | 6 | export class AuthController implements Controller { 7 | 8 | constructor(private readonly authService:AuthService) {} 9 | 10 | initializeRoutes (server: AsklessServer) : void { 11 | server.addRoute.forAuthenticatedUsers.create({ 12 | route: "logout", 13 | handleCreate: async context => { 14 | await this.authService.logout(context.userId); 15 | context.successCallback('OK'); 16 | }, 17 | toOutput: entity => entity, // Always "OK" 18 | }); 19 | 20 | server.addRoute.forAllUsers.create({ 21 | route: "login", 22 | handleCreate: async context => { 23 | if (!context.body["email"]?.length || !context.body["password"]?.length) { 24 | context.errorCallback({ 25 | code: "BAD_REQUEST", 26 | description: "Missing \"email\" or \"password\"" 27 | }); 28 | return; 29 | } 30 | const loginResult = await authService().login(context.body["email"], context.body["password"]); 31 | if (loginResult.isLeft()) { 32 | context.errorCallback(loginResult.error.errorParams); 33 | return; 34 | } 35 | return context.successCallback(loginResult.value) 36 | }, 37 | toOutput: (entity) => TokensModel.fromEntity(entity).output(), 38 | }); 39 | 40 | server.addRoute.forAllUsers.create({ 41 | route: "accessToken", 42 | handleCreate: async context => { 43 | if (!context.body["refreshToken"]?.length || context.body["userId"] == null) { 44 | context.errorCallback({ 45 | code: "BAD_REQUEST", 46 | description: "Missing \"refreshToken\" or \"userId\"" 47 | }); 48 | return; 49 | } 50 | const genResult = await authService().generateNewAccessToken(context.body["userId"], context.body["refreshToken"]); 51 | if (genResult.isLeft()) { 52 | context.errorCallback(genResult.error.errorParams); 53 | return; 54 | } 55 | return context.successCallback(genResult.value) 56 | }, 57 | toOutput: (entity) => TokensModel.fromEntity(entity).output(), 58 | onReceived: () => { console.log("client received token successfully "); }, 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/controllers/message-controller/routes/conversations-with-unreceived-messages-route.ts: -------------------------------------------------------------------------------- 1 | import {MessagesService} from "../../../domain/services/messages-service"; 2 | import {AsklessServer} from "askless"; 3 | 4 | 5 | export class ConversationsWithUnreceivedMessagesRoute { 6 | 7 | constructor(private readonly route:string) {} 8 | 9 | addReadRoute (server:AsklessServer, messagesService:MessagesService) { 10 | return server.addRoute.forAuthenticatedUsers.read({ 11 | route: this.route, 12 | handleRead: async (context) => { 13 | console.log("[READ/LISTEN] conversations-with-unreceived-messages has been called by the client"); // <-- [READ/LISTEN] conversations-with-unreceived-messages has been called by the client 14 | const userId:number = parseInt(context.userId as any); 15 | const conversationsUsersIds = await messagesService.conversationsWithUnreceivedMessages(userId); 16 | console.log(userId+"\": conversations-with-unreceived-messages\" sending -> "+JSON.stringify(conversationsUsersIds)); 17 | if (conversationsUsersIds.includes(userId)) { 18 | throw Error("Ops, incorrect: "+userId); 19 | } 20 | context.successCallback(conversationsUsersIds); 21 | }, 22 | toOutput: (entity) => entity, // conversationsUsersIds 23 | }) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/controllers/message-controller/routes/create-message-route.ts: -------------------------------------------------------------------------------- 1 | import {MessageModel, TextMessageOutput} from "../../../data/models/message-model"; 2 | import {MessagesService} from "../../../domain/services/messages-service"; 3 | import {TextMessageEntity} from "../../../domain/entity/text-message-entity"; 4 | import {AsklessServer} from "askless"; 5 | 6 | 7 | export class CreateMessageRoute { 8 | 9 | constructor(private readonly route:string) {} 10 | 11 | addCreateRoute (server:AsklessServer, messagesService:MessagesService) { 12 | server.addRoute.forAuthenticatedUsers.create({ 13 | route: this.route, 14 | handleCreate: async (context) => { 15 | const senderUserId = context.userId; 16 | const message = MessageModel.fromBody(context.body, senderUserId); 17 | 18 | const entity = await messagesService.createMessage(senderUserId, message.receiverUserId, message); 19 | 20 | context.successCallback(entity); 21 | }, 22 | toOutput: (entity) => MessageModel.fromEntity(entity).output(), 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/controllers/message-controller/routes/messages-were-read-route.ts: -------------------------------------------------------------------------------- 1 | import {TextMessageEntity} from "../../../domain/entity/text-message-entity"; 2 | import {MessagesService} from "../../../domain/services/messages-service"; 3 | import {AsklessServer} from "askless"; 4 | import {ErrorResponse} from "askless/route/ErrorResponse"; 5 | 6 | 7 | export class MessagesWereReadRoute { 8 | 9 | constructor(private readonly route:string) {} 10 | 11 | addCreateRoute (server:AsklessServer, messagesService:MessagesService) { 12 | server.addRoute.forAuthenticatedUsers.create<{ /* entity --> */ readAt: Date }>({ 13 | route: this.route, 14 | handleCreate: async (context) => { 15 | console.log("[CREATE}] \"/messages-were-read\" handler started"); 16 | const loggedUserId:number = context.userId; 17 | const lastMessageId:string = context.body['lastMessageId']; 18 | const lastMessage = (await messagesService.getMessagesByIds([lastMessageId]))[0]; 19 | 20 | if (lastMessage == null) 21 | throw Error('message ' + lastMessageId + ' not found'); 22 | 23 | const senderUserId:number = parseInt(context.body['senderUserId'] as any); 24 | 25 | const readAt = await messagesService.notifyMessagesWereRead(loggedUserId, senderUserId, lastMessage.sentAt); 26 | 27 | context.successCallback({ readAt: readAt }); 28 | }, 29 | toOutput: (entity) => { 30 | return { 31 | readAtMsSinceEpoch: entity.readAt.getTime(), 32 | } 33 | }, 34 | }) 35 | } 36 | 37 | async getMessagesByIdsAndCheckIfLoggedUserIsTheReceiver (loggedUserId: number, messagesIds:string[], messagesService:MessagesService) : Promise> { 38 | const messages:Array = await messagesService.getMessagesByIds(messagesIds); 39 | if(messages.find((message) => message.receiverUserId != loggedUserId)) { 40 | throw new ErrorResponse({code: "PERMISSION_DENIED", description: "You are not allowed to perform to a message that is not yours"}); 41 | } 42 | return messages; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/controllers/message-controller/routes/messages-were-updated-route.ts: -------------------------------------------------------------------------------- 1 | import {TextMessageEntity} from "../../../domain/entity/text-message-entity"; 2 | import {MessageModel, TextMessageOutput} from "../../../data/models/message-model"; 3 | import {MessagesService} from "../../../domain/services/messages-service"; 4 | import {AsklessServer} from "askless"; 5 | 6 | 7 | export class MessagesWereUpdatedRoute { 8 | 9 | constructor(private readonly route:string) {} 10 | 11 | addReadRoute (server: AsklessServer, messagesService:MessagesService) { 12 | return server.addRoute.forAuthenticatedUsers.read({ 13 | route: this.route, 14 | handleRead: async (context) => { 15 | console.log(`[READ] ${this.route} has been called`); 16 | const loggedUserId:number = context.userId; 17 | const userId:number = context.params['userId']; 18 | 19 | const receivedMessages:TextMessageEntity[] = await messagesService.getMessagesWhereSenderHasAnOutdatedVersion(loggedUserId, userId); 20 | 21 | const entity = MessageModel.fromEntityList(receivedMessages); 22 | console.log("read route:"); 23 | console.log(entity); 24 | context.successCallback(entity); 25 | }, 26 | onReceived: async (entity, context) => { 27 | console.log("onReceived messages callback: "+entity.toString() + " messages"); 28 | await messagesService.handleSenderReceivedMessagesUpdate(entity.map(message => message.messageId)); 29 | }, 30 | toOutput: (entities) => MessageModel.fromEntityList(entities).map((model) => model.output()), 31 | }) 32 | } 33 | 34 | } 35 | 36 | // TODO: verificar e remover todos os prints /logs 37 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/controllers/message-controller/routes/read-messages-route.ts: -------------------------------------------------------------------------------- 1 | import {MessagesService} from "../../../domain/services/messages-service"; 2 | import {MessageModel} from "../../../data/models/message-model"; 3 | import {TextMessageEntity} from "../../../domain/entity/text-message-entity"; 4 | import {AsklessServer, AuthenticateUserContext} from "askless"; 5 | import {ReadRouteInstance} from "askless/route/ReadRoute"; 6 | 7 | 8 | export class ReadMessagesRoute { 9 | 10 | constructor(private readonly route:string,) {} 11 | 12 | addReadRoute (server:AsklessServer, messagesService:MessagesService) : ReadRouteInstance, {messages: TextMessageEntity[]}>{ 13 | return server.addRoute.forAuthenticatedUsers.read({ 14 | route: this.route, 15 | onReceived: async (entities, context) => { 16 | console.log("onReceived MESSAGES: "); 17 | await messagesService.handleMessagesUpdate(entities, { receivedAt: new Date(), senderHasOutdatedVersion: true, }); 18 | }, 19 | handleRead: async (context) => { 20 | console.log(`[READ] ${this.route} has been called by the client`); 21 | 22 | const mainUserId:number = parseInt(context.userId as any); 23 | const senderUserId:number = context.params['senderUserId']; 24 | 25 | const entities = await messagesService.getMessages(mainUserId, senderUserId); 26 | context.successCallback(entities); 27 | }, 28 | toOutput: (entities) => MessageModel.fromEntityList(entities).map((model) => model.output()), 29 | }) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/controllers/message-controller/routes/typing-route.ts: -------------------------------------------------------------------------------- 1 | import {MessagesService} from "../../../domain/services/messages-service"; 2 | import {AsklessServer} from "askless"; 3 | 4 | 5 | export class TypingRoute { 6 | 7 | constructor(private readonly route: string) {} 8 | 9 | addReadRoute (server:AsklessServer, messagesService:MessagesService,) { 10 | return server.addRoute.forAuthenticatedUsers.read<"TYPING" | "NOT_TYPING">({ 11 | route: this.route, 12 | handleRead: async (context) => { 13 | const loggedUserId:number = context.userId; 14 | const typingUserId:number = context.params['typingUserId']; 15 | 16 | console.log("READ TypingRoute "+loggedUserId+" STARTED LISTENING typingUserId = "+typingUserId); 17 | 18 | context.successCallback(messagesService.isTyping(typingUserId, loggedUserId) ? "TYPING" : "NOT_TYPING"); 19 | }, 20 | toOutput: (entity) => entity, // "TYPING" or "NOT_TYPING" 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/controllers/message-controller/routes/user-typed-route.ts: -------------------------------------------------------------------------------- 1 | import {MessagesService} from "../../../domain/services/messages-service"; 2 | import {AsklessServer} from "askless"; 3 | 4 | 5 | export class UserTypedRoute { 6 | 7 | constructor(private readonly route:string) {} 8 | 9 | addCreateRoute (server:AsklessServer, messagesService:MessagesService) { 10 | server.addRoute.forAuthenticatedUsers.create({ 11 | route: this.route, 12 | toOutput: (entity) => entity, // Always "OK" 13 | handleCreate: async (context) => { 14 | const loggedUserId:number = context.userId; 15 | const receiverUserId:number = context.body['receiverUserId']; 16 | 17 | messagesService.notifyUserIsTyping(loggedUserId, receiverUserId); 18 | 19 | context.successCallback('OK'); 20 | } 21 | }) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/controllers/user-controller/user-controller.ts: -------------------------------------------------------------------------------- 1 | import {UserModel} from "../../data/models/user-model"; 2 | import {UsersService, UsersServiceParams} from "../../domain/services/users-service"; 3 | import {Controller} from "../../domain/controllers-and-services"; 4 | import {UserEntity} from "../../domain/entity/user-entity"; 5 | import {AsklessServer, AuthenticateUserContext} from "askless"; 6 | import {ReadRouteInstance} from "askless/route/ReadRoute"; 7 | 8 | 9 | export class UserController implements Controller { 10 | private userListRouteInstance: ReadRouteInstance>; 11 | private readonly usersService:UsersService; 12 | 13 | constructor(initUsersService:(params:UsersServiceParams) => UsersService) { 14 | this.usersService = initUsersService({ 15 | notifyNewUserWasCreated: (userId:number) => { 16 | this.userListRouteInstance.notifyChanges({ 17 | where: context => { 18 | return context.userId != userId; 19 | } 20 | }) 21 | } 22 | }); 23 | } 24 | 25 | initializeRoutes (server: AsklessServer) : void { 26 | server.addRoute.forAllUsers.create({ 27 | route: 'user', 28 | handleCreate: async (context) => { 29 | const user = await UserModel.fromBody(context.body); 30 | const res = await this.usersService.saveUser(user); 31 | if(res.isRight()){ 32 | context.successCallback(res.value); 33 | return; 34 | } 35 | context.errorCallback(res.error.errorParams); 36 | }, 37 | toOutput: (entity) => UserModel.fromEntity(entity).output(), 38 | }); 39 | 40 | this.userListRouteInstance = server.addRoute.forAuthenticatedUsers.read({ 41 | route: 'user-list', 42 | handleRead: async (context) => { 43 | console.log("user-list: read started"); 44 | const mainUserId:number = context.userId; 45 | if (mainUserId == null) { 46 | context.errorCallback({ 47 | description: "Only logged users can perform this operation", 48 | code: "FORBIDDEN", 49 | }) 50 | return; 51 | } 52 | const users = await this.usersService.getAllUsers({ exceptUserId: mainUserId }); 53 | 54 | context.successCallback(users.sort((a,b) => { 55 | const aName = `${a.firstName} ${a.lastName}`; 56 | const bName = `${b.firstName} ${b.lastName}`; 57 | return aName.localeCompare(bName); 58 | })); 59 | }, 60 | toOutput: (entities) => { 61 | return UserModel.fromEntityList(entities).map((user) => user.output()) 62 | }, 63 | }); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/data/data-source/db-datasouce.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata" 2 | import { DataSource } from "typeorm" 3 | import {dbDatasourceOptions} from "../../environment/db"; 4 | 5 | export const AppDataSource = new DataSource(dbDatasourceOptions); 6 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/data/models/chat-content-model.ts: -------------------------------------------------------------------------------- 1 | import {ChatContentEntity} from "../../domain/entity/chat-content-entity"; 2 | import {MessageModel, TextMessageOutput} from "./message-model"; 3 | 4 | export interface ChatContentOutputToClient { 5 | [ChatContentModel.kMessages]: TextMessageOutput[], 6 | [ChatContentModel.kIsTyping]: boolean, 7 | } 8 | 9 | export class ChatContentModel extends ChatContentEntity{ 10 | static readonly kMessages = "messages"; 11 | static readonly kIsTyping = "isTyping"; 12 | 13 | output() : ChatContentOutputToClient { 14 | return { 15 | [ChatContentModel.kMessages]: this.messages.map((message) => MessageModel.fromEntity(message).output()), 16 | [ChatContentModel.kIsTyping]: this.isTyping 17 | } 18 | } 19 | 20 | static fromEntity(entity: ChatContentEntity) : ChatContentModel { 21 | return new ChatContentModel(entity.messages, entity.isTyping); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/data/models/message-model.ts: -------------------------------------------------------------------------------- 1 | import {TextMessageEntity} from "../../domain/entity/text-message-entity"; 2 | import {AsklessError} from "askless"; 3 | 4 | /** output field keys */ 5 | const kMessageId = "messageId"; 6 | const kSentAtMsSinceEpoch = "sentAtMsSinceEpoch"; 7 | const kReceivedAtMsSinceEpoch = "receivedAtMsSinceEpoch"; 8 | const kReadAtMsSinceEpoch = "readAtMsSinceEpoch"; 9 | const kSenderUserId = "senderUserId"; 10 | const kReceiverUserId = "receiverUserId"; 11 | const kText = "text"; 12 | 13 | export interface TextMessageOutput { 14 | [kMessageId]: string; 15 | [kSentAtMsSinceEpoch]: number; 16 | [kReceivedAtMsSinceEpoch]: number; 17 | [kSenderUserId]: number; 18 | [kReceiverUserId]: number; 19 | [kText]: string; 20 | } 21 | 22 | export interface TextMessageInput { 23 | [kMessageId]: string; 24 | [kReceiverUserId]: number; 25 | [kText]: string; 26 | } 27 | 28 | export class MessageModel extends TextMessageEntity { 29 | 30 | output() : TextMessageOutput { 31 | return Object.assign({}, { 32 | [kMessageId]: this.messageId, 33 | [kSentAtMsSinceEpoch]: this.sentAt.getTime(), 34 | [kReceivedAtMsSinceEpoch]: this.receivedAt?.getTime(), 35 | [kReadAtMsSinceEpoch]: this.readAt?.getTime(), 36 | [kSenderUserId]: this.senderUserId, 37 | [kReceiverUserId]: this.receiverUserId, 38 | [kText]: this.text, 39 | }); 40 | } 41 | 42 | static fromEntity(entity: TextMessageEntity) : MessageModel { 43 | return Object.assign(new MessageModel(), entity); 44 | } 45 | 46 | static fromBody(data: TextMessageInput, senderUserId:number) : MessageModel { 47 | if(MessageModel.invalid(data)) { 48 | throw new AsklessError({code: "BAD_REQUEST", description: MessageModel.validationError(data)}); 49 | } 50 | const res = new MessageModel(); 51 | res.messageId = data.messageId; 52 | res.text = data.text; 53 | res.senderUserId = senderUserId; 54 | res.receiverUserId = data.receiverUserId; 55 | return res; 56 | } 57 | 58 | static fromEntityList(receivedMessages: TextMessageEntity[]) : MessageModel[] { 59 | return receivedMessages.map((msg) => MessageModel.fromEntity(msg)); 60 | } 61 | 62 | private static validationError (data:TextMessageInput) { 63 | const separator = '; '; 64 | let errors = ""; 65 | if (!data.messageId?.length) { 66 | errors += `Generate a random string of 28 characters for the "${kMessageId}" field${separator}`; 67 | } 68 | if (!data.text?.length) { 69 | errors += `"${kText}" is null or empty${separator}`; 70 | } 71 | if (!data.receiverUserId) { 72 | errors += `"${kReceiverUserId}" is null${separator}`; 73 | } 74 | const res = errors.substring(0, errors.length - separator.length); 75 | if (!res.length) { 76 | return null; 77 | } 78 | return res; 79 | } 80 | private static invalid(data: TextMessageInput) { 81 | return Boolean(this.validationError(data)?.length); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/data/models/tokens-model.ts: -------------------------------------------------------------------------------- 1 | import {TokensEntity} from "../../domain/entity/tokens-entity"; 2 | 3 | export interface TokensOutputToClient { 4 | [TokensModel.kAccessToken]: string; 5 | [TokensModel.kRefreshToken]: string; 6 | [TokensModel.kUserId]: number; 7 | [TokensModel.kAccessTokenExpirationMsSinceEpoch]: number; 8 | } 9 | 10 | export class TokensModel extends TokensEntity { 11 | static readonly kAccessToken = "accessToken"; 12 | static readonly kRefreshToken = "refreshToken"; 13 | static readonly kUserId = "userId"; 14 | static readonly kAccessTokenExpirationMsSinceEpoch = "accessTokenExpirationMsSinceEpoch"; 15 | 16 | output() : TokensOutputToClient { 17 | return { 18 | [TokensModel.kAccessToken]: this.accessToken, 19 | [TokensModel.kRefreshToken]: this.refreshToken, 20 | [TokensModel.kUserId]: this.userId, 21 | [TokensModel.kAccessTokenExpirationMsSinceEpoch]: this.accessTokenExpiration.getTime(), 22 | } 23 | } 24 | 25 | static fromEntity (entity:TokensEntity) : TokensModel { 26 | return Object.assign(new TokensModel(null,null,null,null), entity); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/data/models/user-model.ts: -------------------------------------------------------------------------------- 1 | import {UserEntity} from "../../domain/entity/user-entity"; 2 | import {hashEncryption} from "../../utils/encryption-utils"; 3 | import {AsklessError} from "askless"; 4 | 5 | 6 | const kUserId = "userId"; 7 | const kCreatedAtMsSinceEpoch = "createdAtMsSinceEpoch"; 8 | const kFirstName = "firstName"; 9 | const kLastName = "lastName"; 10 | const kEmail = "email"; 11 | const kPassword = "password"; 12 | 13 | export interface UserOutputToClient { 14 | [kUserId]: number, 15 | [kCreatedAtMsSinceEpoch]: number, 16 | [kFirstName]: string, 17 | [kLastName]: string, 18 | } 19 | export interface UserBodyFromClient { 20 | [kUserId]: number, 21 | [kFirstName]: string, 22 | [kLastName]: string, 23 | [kEmail]: string, 24 | [kPassword]: string, 25 | } 26 | 27 | export class UserModel extends UserEntity { 28 | 29 | output() : UserOutputToClient { 30 | return { 31 | [kUserId]: this.userId, 32 | [kFirstName]: this.firstName, 33 | [kLastName]: this.lastName, 34 | [kCreatedAtMsSinceEpoch]: this.createdAt.getTime(), 35 | } 36 | } 37 | 38 | static toClient(entity:UserEntity) : object { 39 | return Object.assign(new UserModel(), entity).output(); 40 | } 41 | 42 | static async fromBody(data: UserBodyFromClient) : Promise { 43 | if(UserModel.invalid(data)) { 44 | throw new AsklessError({code: "BAD_REQUEST", description: UserModel.validationError(data)}); 45 | } 46 | const res = new UserModel(); 47 | res.userId = data.userId; 48 | res.firstName = data.firstName; 49 | res.lastName = data.lastName; 50 | res.email = data.email; 51 | res.passwordHash = await hashEncryption(data.password); 52 | return res; 53 | } 54 | 55 | private static invalid(data: UserBodyFromClient) : boolean { 56 | return Boolean(UserModel.validationError(data)?.length); 57 | } 58 | 59 | private static validationError(data:UserBodyFromClient) : string | null { 60 | const separator = '; '; 61 | let errors = ""; 62 | if(!data.firstName?.length) { 63 | errors += `Missing '${kFirstName}'${separator}`; 64 | } 65 | if(!data.lastName?.length) { 66 | errors += `Missing '${kLastName}'${separator}`; 67 | } 68 | if(!data.email?.length) { 69 | errors += `Missing '${kEmail}'${separator}`; 70 | } 71 | if(!data.password?.length) { 72 | errors += `Missing '${kPassword}'${separator}`; 73 | } 74 | if(errors?.length){ 75 | console.error(errors); 76 | } 77 | return errors.length ? errors.substring(0, errors.length - separator.length) : null; 78 | } 79 | 80 | static fromEntity(entity: UserEntity) : UserModel { 81 | return Object.assign(new UserModel(), entity); 82 | } 83 | 84 | static fromEntityList(users: UserEntity[]) { 85 | return users.map((u) => UserModel.fromEntity(u)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/domain/controllers-and-services.ts: -------------------------------------------------------------------------------- 1 | import {UserController} from "../controllers/user-controller/user-controller"; 2 | import {MessageController} from "../controllers/message-controller/message-controller"; 3 | import {AuthController} from "../controllers/auth-controller/auth-controller"; 4 | import {MessagesService} from "./services/messages-service"; 5 | import {AuthService} from "./services/auth-service"; 6 | import {UsersService} from "./services/users-service"; 7 | import {AsklessServer} from "askless"; 8 | 9 | export interface Controller { 10 | initializeRoutes (server: AsklessServer) : void; 11 | } 12 | 13 | let _usersService:UsersService; 14 | let _messagesService:MessagesService; 15 | let _authService:AuthService; 16 | let _authController:AuthController; 17 | let _controllers:Array; 18 | 19 | export const controllers = () => _controllers; 20 | 21 | export function initializeControllers (server:AsklessServer) { 22 | _controllers = [ 23 | new UserController(params => _usersService = new UsersService(params)), 24 | new MessageController(params => _messagesService = new MessagesService(params)), 25 | _authController = new AuthController(_authService = new AuthService(_usersService, server)), 26 | ]; 27 | return _controllers; 28 | } 29 | 30 | export function authController() { return _authController; } 31 | export function authService() { return _authService; } 32 | export function messagesService() { return _messagesService; } 33 | export function usersService() { return _usersService; } 34 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/domain/entity/chat-content-entity.ts: -------------------------------------------------------------------------------- 1 | import {TextMessageEntity} from "./text-message-entity"; 2 | 3 | 4 | export class ChatContentEntity { 5 | 6 | constructor( 7 | public readonly messages: TextMessageEntity[], 8 | public readonly isTyping:boolean, 9 | ) {} 10 | 11 | } 12 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/domain/entity/failures/duplicate-email-failure.ts: -------------------------------------------------------------------------------- 1 | import {Failure} from "./failure"; 2 | import {AsklessErrorParams} from "askless/client/response/AsklessError"; 3 | 4 | 5 | export class DuplicateEmailFailure extends Failure { 6 | 7 | constructor(public readonly email:string, ) { 8 | super(`The email ${email} is already registered`); 9 | } 10 | 11 | // override 12 | get errorParams(): AsklessErrorParams { 13 | return { 14 | code: "DUPLICATED_EMAIL", 15 | description: this.description!, 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/domain/entity/failures/failure.ts: -------------------------------------------------------------------------------- 1 | import {AsklessErrorParams} from "askless/client/response/AsklessError"; 2 | import {AsklessError, AsklessErrorCode} from "askless"; 3 | 4 | 5 | export class Failure { 6 | 7 | constructor(public readonly description?:string) {} 8 | 9 | get errorParams() : AsklessErrorParams { 10 | return new AsklessError({ 11 | code: AsklessErrorCode.INTERNAL_ERROR, 12 | description: this.description ?? "An internal error occurred" 13 | }) 14 | }; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/domain/entity/failures/invalid-email-failure.ts: -------------------------------------------------------------------------------- 1 | import {Failure} from "./failure"; 2 | import {AsklessErrorParams} from "askless/client/response/AsklessError"; 3 | 4 | 5 | export class InvalidEmailFailure extends Failure { 6 | 7 | get errorParams(): AsklessErrorParams { 8 | return { code: "INVALID_EMAIL" }; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/domain/entity/failures/invalid-password-failure.ts: -------------------------------------------------------------------------------- 1 | import {Failure} from "./failure"; 2 | import {AsklessErrorParams} from "askless/client/response/AsklessError"; 3 | 4 | 5 | export class InvalidPasswordFailure extends Failure { 6 | 7 | get errorParams(): AsklessErrorParams { 8 | return { code: "INVALID_PASSWORD" }; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/domain/entity/failures/invalid-refresh-token-failure.ts: -------------------------------------------------------------------------------- 1 | import {Failure} from "./failure"; 2 | import {AsklessErrorParams} from "askless/client/response/AsklessError"; 3 | 4 | 5 | export class InvalidRefreshTokenFailure extends Failure { 6 | 7 | // override 8 | get errorParams () : AsklessErrorParams { 9 | return { 10 | code: 'INVALID_REFRESH_TOKEN', 11 | description: 'The refresh token is invalid' 12 | } 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/domain/entity/text-message-entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | PrimaryGeneratedColumn, 4 | Column, 5 | OneToMany, 6 | ManyToOne, 7 | JoinColumn, 8 | CreateDateColumn, 9 | PrimaryColumn 10 | } from "typeorm" 11 | import {UserEntity} from "./user-entity"; 12 | 13 | @Entity({name: 'text_message'}) 14 | export class TextMessageEntity { 15 | 16 | /** 17 | * `messageId` is a random string generated in the App side, 18 | * the message will be saved offline in the App first, and 19 | * will be sent to the server afterward 20 | * */ 21 | @PrimaryColumn({length: 28}) 22 | messageId: string 23 | 24 | @Column() 25 | sentAt: Date 26 | 27 | @Column({nullable: true}) 28 | receivedAt?: Date 29 | 30 | @Column({nullable: false, default: false}) 31 | senderHasOutdatedVersion?: boolean 32 | 33 | @Column({nullable: true}) 34 | readAt?:Date; 35 | 36 | @Column() 37 | text: string 38 | 39 | @Column({unsigned: true}) 40 | senderUserId: number 41 | 42 | @Column({unsigned: true}) 43 | receiverUserId: number 44 | 45 | @ManyToOne(type => UserEntity, (user) => user.messagesSent, {lazy: true, nullable: false}) 46 | sender:Promise 47 | 48 | @ManyToOne(type => UserEntity, (user) => user.messagesReceived, {lazy: true, nullable: false}) 49 | receiver:Promise 50 | 51 | } 52 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/domain/entity/tokens-entity.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export class TokensEntity { 5 | 6 | constructor( 7 | public readonly userId:number, 8 | public readonly accessToken:string, 9 | public readonly accessTokenExpiration:Date, 10 | public readonly refreshToken:string, 11 | ) {} 12 | } -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/domain/entity/typing-indicator-entity.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** TypingIndicatorEntity is not stored in the database */ 4 | export class TypingIndicatorEntity { 5 | 6 | constructor( 7 | public readonly senderUserId:number, 8 | public readonly receiverUserId:number 9 | ) {} 10 | } -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/domain/entity/user-entity.ts: -------------------------------------------------------------------------------- 1 | import {Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn, CreateDateColumn} from "typeorm" 2 | import {TextMessageEntity} from "./text-message-entity"; 3 | 4 | @Entity({name: "user"}) 5 | export class UserEntity { 6 | 7 | @PrimaryGeneratedColumn({unsigned: true}) 8 | userId: number 9 | 10 | @CreateDateColumn() 11 | createdAt: Date 12 | 13 | @Column() 14 | firstName: string 15 | 16 | @Column() 17 | lastName: string 18 | 19 | @Column({unique: true}) 20 | email: string 21 | 22 | @Column({name: "password_hash"}) 23 | passwordHash: string 24 | 25 | @Column({name: "refresh_token_hash", nullable: true}) 26 | refreshTokenHash?: string 27 | 28 | @OneToMany(type => TextMessageEntity, (message) => message.sender, {lazy: true}) 29 | messagesSent:Promise 30 | 31 | @OneToMany(type => TextMessageEntity, (message) => message.receiver, {lazy: true}) 32 | messagesReceived:Promise 33 | 34 | } 35 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/domain/services/users-service.ts: -------------------------------------------------------------------------------- 1 | import {UserEntity} from "../entity/user-entity"; 2 | import {AppDataSource} from "../../data/data-source/db-datasouce"; 3 | import {DuplicateEmailFailure} from "../entity/failures/duplicate-email-failure"; 4 | import {Either, Left, Right} from "../../utils/either"; 5 | import {Equal, In, Not} from "typeorm"; 6 | import {Failure} from "../entity/failures/failure"; 7 | 8 | export type UsersServiceParams = { 9 | notifyNewUserWasCreated: (userId:number) => void 10 | }; 11 | export class UsersService { 12 | constructor(private readonly params: UsersServiceParams) {} 13 | 14 | private readonly _usersTypeormRepo = AppDataSource.getRepository(UserEntity); 15 | 16 | async updateUser(userId:number, updateData: Partial>) : Promise { 17 | await AppDataSource.manager.update(UserEntity, userId, updateData); 18 | } 19 | 20 | async saveUser(user:UserEntity) : Promise> { 21 | try { 22 | user = Object.assign(new UserEntity(), user); 23 | user = await this._usersTypeormRepo.save(user); 24 | this.params.notifyNewUserWasCreated(user.userId); 25 | return Right.create(user); 26 | } catch (e) { 27 | if(e.code == "ER_DUP_ENTRY") { 28 | return Left.create(new DuplicateEmailFailure(user.email)); 29 | } else if (e.code?.length) { 30 | throw `TODO: ${e.code}`; 31 | } 32 | console.error("saveUser error:"); 33 | console.error(e.toString()); 34 | return Left.create(new Failure()); 35 | } 36 | } 37 | 38 | async getAllUsers(params?:{exceptUserId?:number}) : Promise { 39 | if (params?.exceptUserId == null) { 40 | return this._usersTypeormRepo.find(); 41 | } 42 | return this._usersTypeormRepo.find({ 43 | where: { 44 | userId: Not(params.exceptUserId) 45 | } 46 | }) 47 | } 48 | 49 | getUserByEmail(email: string) : Promise { 50 | return this._usersTypeormRepo.findOneBy({email: email}); 51 | } 52 | getUserById(userId:number) : Promise { 53 | return this._usersTypeormRepo.findOneBy({userId: userId}); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/environment/db.ts: -------------------------------------------------------------------------------- 1 | import {DataSourceOptions} from "typeorm/data-source/DataSourceOptions"; 2 | import {TextMessageEntity} from "../domain/entity/text-message-entity"; 3 | import {UserEntity} from "../domain/entity/user-entity"; 4 | 5 | export const dbDatasourceOptions: DataSourceOptions = { 6 | // TODO: replace with your database configuration in the fields bellow: 7 | type: "mysql", 8 | host: "127.0.0.1", //localhost 9 | port: 3306, 10 | username: "root", 11 | password: "root", 12 | database: "flutter_chat_app_with_nodejs", 13 | 14 | // No need to change this fields bellow 15 | synchronize: true, 16 | logging: false, 17 | charset : 'utf8mb4', 18 | entities: [TextMessageEntity, UserEntity], 19 | migrations: [], 20 | subscribers: [], 21 | } 22 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/environment/jwt-private.key: -------------------------------------------------------------------------------- 1 | (change with your own random text) 2 | my unique and private jwt key 123 3 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { AppDataSource } from "./data/data-source/db-datasouce"; 2 | import {authController, controllers, initializeControllers} from "./domain/controllers-and-services"; 3 | import {verifyJwtAccessToken} from "./utils/jwt-utils"; 4 | import {AsklessServer} from "askless"; 5 | 6 | 7 | AppDataSource.initialize().then(async () => { 8 | const server = new AsklessServer(); 9 | 10 | initializeControllers(server); 11 | 12 | // initializing all controllersAndServices 13 | for (let controller of controllers()) { 14 | controller.initializeRoutes(server); 15 | } 16 | 17 | server.init({ 18 | wsOptions: { port: 3000, }, 19 | debugLogs: false, 20 | sendInternalErrorsToClient: false, 21 | requestTimeoutInMs: 7 * 1000, 22 | authenticate: async (credential, accept, reject) : Promise => { 23 | if (credential && credential["accessToken"]) { 24 | const result = verifyJwtAccessToken(credential["accessToken"]); 25 | if (!result.valid) { 26 | reject({credentialErrorCode: "EXPIRED_ACCESS_TOKEN"}); 27 | return; 28 | } 29 | accept.asAuthenticatedUser({ userId: result.userId, }); 30 | return; 31 | } 32 | 33 | reject({credentialErrorCode: "MISSING_CREDENTIAL"}); 34 | }, 35 | }); 36 | 37 | server.start(); 38 | console.log("started on "+server.localUrl); 39 | 40 | }).catch(databaseError => console.log(databaseError)) 41 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/utils/either.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Source: 3 | * https://dev.to/milos192/error-handling-with-the-either-type-2b63 4 | * */ 5 | 6 | export type Either = Left | Right; 7 | 8 | export class Left { 9 | readonly error: T; 10 | 11 | private constructor(error: T) { 12 | this.error = error; 13 | } 14 | 15 | isLeft(): this is Left { 16 | return true; 17 | } 18 | 19 | isRight(): this is Right { 20 | return false; 21 | } 22 | 23 | static create(error: U): Left { 24 | return new Left(error); 25 | } 26 | } 27 | 28 | export class Right { 29 | readonly value: T; 30 | 31 | private constructor(value: T) { 32 | this.value = value; 33 | } 34 | 35 | isLeft(): this is Left { 36 | return false; 37 | } 38 | 39 | isRight(): this is Right { 40 | return true; 41 | } 42 | 43 | static create(value: U): Right { 44 | return new Right(value); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/utils/encryption-utils.ts: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcrypt") 2 | 3 | /** Generates a hash by using a random generated salt */ 4 | export async function hashEncryption (unecryptedSecret:string) : Promise { 5 | const salt = await randomSalt(); 6 | return await bcrypt.hash(unecryptedSecret, salt); 7 | } 8 | 9 | /** Verifies if the unecryptedSecret and the ecryptedSecret matches */ 10 | export async function verify(unecryptedSecret:string, ecryptedSecret:string) : Promise { 11 | return bcrypt.compare(unecryptedSecret, ecryptedSecret); 12 | } 13 | 14 | async function randomSalt() : Promise { 15 | return await bcrypt.genSalt(10); 16 | } -------------------------------------------------------------------------------- /nodejs_websocket_backend/src/utils/jwt-utils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import * as jwt from "jsonwebtoken"; 4 | 5 | const privateKey = fs.readFileSync(path.join(__dirname, '../', 'environment', 'jwt-private.key'), {encoding: "utf-8"}); 6 | const expiresInSeconds:number = 30 * 60; 7 | 8 | export function generateAccessToken (userId:number) : { accessToken:string, accessTokenExpiration:Date } { 9 | const accessTokenExpirationMsSinceEpoch:number = Date.now() + (expiresInSeconds * 1000); 10 | return { 11 | 'accessToken': jwt.sign({ userId: userId }, privateKey, { 12 | expiresIn: expiresInSeconds 13 | }), 14 | 'accessTokenExpiration': new Date(accessTokenExpirationMsSinceEpoch) 15 | } 16 | } 17 | export function verifyJwtAccessToken(jwtAccessToken:string) : { userId?:number, valid:boolean, claims?:string[], locals? } { 18 | try { 19 | const res = jwt.verify(jwtAccessToken, privateKey); 20 | return { 21 | valid: true, 22 | userId: (res as any).userId, 23 | // optionally set the user claims and locals here 24 | claims: [], 25 | locals: {}, 26 | }; 27 | } catch (e) { 28 | return { valid: false }; 29 | } 30 | } 31 | export function generateRefreshToken() { 32 | //https://stackoverflow.com/a/8084248/4508758 33 | return (Math.random() + 1).toString(36).substring(2); 34 | } 35 | -------------------------------------------------------------------------------- /nodejs_websocket_backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es5", 5 | "es6" 6 | ], 7 | "target": "es5", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "strictNullChecks": false, 11 | "outDir": "./build", 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "sourceMap": true, 15 | } 16 | } --------------------------------------------------------------------------------