├── .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 | }
--------------------------------------------------------------------------------