101 | VERBATIM
102 | )
103 | add_custom_target(flutter_assemble DEPENDS
104 | "${FLUTTER_LIBRARY}"
105 | ${FLUTTER_LIBRARY_HEADERS}
106 | ${CPP_WRAPPER_SOURCES_CORE}
107 | ${CPP_WRAPPER_SOURCES_PLUGIN}
108 | ${CPP_WRAPPER_SOURCES_APP}
109 | )
110 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Flutter Firebase Chat Core
10 |
11 |
12 | Actively maintained, community-driven Firebase BaaS for chat applications with an optional chat UI.
13 |
14 |
15 |
16 |
17 |
18 | 🇺🇦🇺🇦 We are Ukrainians. If you enjoy our work, please consider donating to help save our country. 🇺🇦🇺🇦
19 |
20 |
21 |
22 |
23 |
24 | ⚠️⚠️ Recommended for small or PoC projects, might not be optimized for large amounts of data. I suggest to use this on a free Spark plan, otherwise be extremely cautious. ⚠️⚠️
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Flyer Chat is a platform for creating in-app chat experiences using Flutter or [React Native](https://github.com/flyerhq/react-native-firebase-chat-core). This repository contains Firebase BaaS implementation for Flutter. We are also working on our more advanced SaaS and self-hosted solutions.
52 |
53 | * **Free, open-source and community-driven**. We offer no paid plugins and strive to create an easy-to-use, almost drop-in chat experience for any application. Contributions are more than welcome! Please read our [Contributing Guide](CONTRIBUTING.md).
54 |
55 | * **Chat UI agnostic**. You can choose the chat UI you prefer. But if you don't have one, we provide our own free and open-source [Flutter Chat UI](https://pub.dev/packages/flutter_chat_ui), which can be used to create a working chat in minutes.
56 |
57 | * **Easy to use**. Returns streams of data for messages, rooms and users. [Firebase Security Rules](https://firebase.google.com/docs/rules) control access to the data. Check our [documentation](https://docs.flyer.chat/flutter/firebase/firebase-overview) for the info.
58 |
59 | ## Getting Started
60 |
61 | ### Requirements
62 |
63 | `Dart >=2.19.0` and `Flutter >=3.0.0`, [Firebase](https://firebase.google.com) project.
64 |
65 | Read our [documentation](https://docs.flyer.chat/flutter/firebase/firebase-overview) or see the [example](https://github.com/flyerhq/flutter_firebase_chat_core/tree/main/example) project. To run the example project you need to have your own [Firebase](https://firebase.google.com) project and then follow steps 1 and 2 of [Add Firebase to your Flutter app](https://firebase.google.com/docs/flutter/setup), override `firebase_options.dart`, don't commit it though 😉
66 |
67 | After all of this is done you will need to register a couple of users and the example app will automatically suggest email and password on the register screen, default password is `Qawsed1-`. To set up [Firebase Security Rules](https://firebase.google.com/docs/rules) so users can see only the data they should see, continue with our [documentation](https://docs.flyer.chat/flutter/firebase/firebase-rules).
68 |
69 | ## Contributing
70 |
71 | Please read our [Contributing Guide](CONTRIBUTING.md) before submitting a pull request to the project.
72 |
73 | ## Code of Conduct
74 |
75 | Flyer Chat has adopted the [Contributor Covenant](https://www.contributor-covenant.org) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
76 |
77 | ## License
78 |
79 | Licensed under the [Apache License, Version 2.0](LICENSE)
80 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 1.6.8
2 |
3 | - **BREAKING CHANGE**: Update firebase to the latest SDK.
4 |
5 | ## 1.6.7
6 |
7 | - Update dependencies. Requires Dart >= 2.19.0.
8 |
9 | ## 1.6.4
10 |
11 | - Update dependencies. Requires Dart >= 2.18.0.
12 |
13 | ## 1.6.3
14 |
15 | - Decrease amount of DB reads for the create room call. Thanks @gmgm60 for the PR!
16 | - Update docs. Thanks @lrsvmb for the PR!
17 | - Update dependencies
18 |
19 | ## 1.6.2
20 |
21 | - Code refactor
22 |
23 | ## 1.6.1
24 |
25 | - Update example to work with FlutterFire CLI
26 | - When creating a group chat set the creator to `admin` by default. Thanks @adetayoadeyemi for the PR!
27 |
28 | ## 1.6.0
29 |
30 | - Update to Flutter 3
31 | - Update a room `updatedAt` when a new message is sent. Thanks @rootd for the PR!
32 |
33 | ## 1.5.4
34 |
35 | - Fix `lastSeen` user property when using `createUserInFirestore`. Thanks @AcnoSaga for reporting!
36 | - Update dependencies
37 |
38 | ## 1.5.3
39 |
40 | - Update to Flutter 2.10.4. Requires Dart >= 2.16.0.
41 | - Update dependencies
42 |
43 | ## 1.5.2
44 |
45 | - Add an option to specify a custom Firebase app through the config. Thanks @valas69 for reporting!
46 | - Add `deleteRoom` and `deleteMessage` methods. Thanks @foxdev-flutter for the PR!
47 | - Add some pagination properties to the query. Thanks @awesomejerry for the PR!
48 | - Add `updateRoom` method
49 | - Update dependencies. Requires Dart >= 2.15.1.
50 |
51 | ## 1.5.1
52 |
53 | - Update dependencies
54 |
55 | ## 1.5.0
56 |
57 | - Update dependencies (requires Dart >=2.14.0)
58 |
59 | ## 1.4.2
60 |
61 | - Add an option to rename default collections, `rooms` and `users`
62 |
63 | ## 1.4.1
64 |
65 | - Fix release on `pub.dev`
66 |
67 | ## 1.4.0
68 |
69 | - Update to Flutter 2.5
70 |
71 | ## 1.3.2
72 |
73 | - Update dependencies
74 |
75 | ## 1.3.1
76 |
77 | - Update dependencies
78 |
79 | ## 1.3.0
80 |
81 | - Set room's `lastMessages`. Thanks @mashegoindustries for reporting!
82 | - Align version with https://pub.dev/packages/flutter_chat_ui.
83 | - Update dependencies
84 |
85 | ## 1.1.4
86 |
87 | - Add an option to send metadata
88 | - Update dependencies
89 |
90 | ## 1.1.3
91 |
92 | - Update dependencies
93 |
94 | ## 1.1.2
95 |
96 | - Update dependencies
97 |
98 | ## 1.1.1
99 |
100 | - Update dependencies
101 | - Add possibility to order rooms by last updated (e.g. show room on top when it has the latest message). See documentation comment for `rooms` function inside `FirebaseChatCore` class.
102 | - Fix user avatars inside example if no remote URL is available
103 | - Add `deleteUserFromFirestore` function. Thanks @SalahAdDin for the PR!
104 |
105 | ## 1.1.0
106 |
107 | This release marks a major chat architecture overhaul based on a community feedback. In the future we don't expect such big changes in one release and will try to do backwards compatible code as much as possible.
108 |
109 | - **BREAKING CHANGE**: [FileMessage] `fileName` is renamed to `name`
110 | - **BREAKING CHANGE**: [ImageMessage] `imageName` is renamed to `name`
111 | - **BREAKING CHANGE**: [Messages] `timestamp` is renamed to `createdAt`
112 | - **BREAKING CHANGE**: [Status] `read` is renamed to `seen`
113 | - **BREAKING CHANGE**: [User] `avatarUrl` is renamed to `imageUrl`
114 | - New `custom` and `unsupported` message types. First one is used to build any message you want, second one is to support backwards compatibility
115 |
116 | ## 1.0.4
117 |
118 | - Update dependencies
119 |
120 | ## 1.0.3
121 |
122 | - **BREAKING CHANGE**: Updated `cloud_firestore` to version 2
123 | - Update to Flutter 2.2
124 |
125 | ## 1.0.2
126 |
127 | - Fix static analysis warning
128 |
129 | ## 1.0.1
130 |
131 | - Add metadata to the room class for easier extensibility. Thanks @alihen for the PR!
132 |
133 | ## 1.0.0
134 |
135 | - Public release
136 |
137 | ## 0.3.0
138 |
139 | - Add docs
140 |
141 | ## 0.2.0
142 |
143 | - Update types
144 |
145 | ## 0.1.2
146 |
147 | - Add documentation comments
148 |
149 | ## 0.1.1
150 |
151 | - Fix static analysis warning
152 |
153 | ## 0.1.0
154 |
155 | - Update to Flutter 2
156 |
157 | ## 0.0.8
158 |
159 | - Update to the latest chat UI
160 |
161 | ## 0.0.7
162 |
163 | - Handle attachments upload
164 |
165 | ## 0.0.6
166 |
167 | - Update message when preview data fetched
168 |
169 | ## 0.0.5
170 |
171 | - Update LICENSE
172 |
173 | ## 0.0.4
174 |
175 | - Finish core
176 |
177 | ## 0.0.3
178 |
179 | - Added chat UI
180 |
181 | ## 0.0.2
182 |
183 | - Added example
184 |
185 | ## 0.0.1
186 |
187 | - Initial release
188 |
--------------------------------------------------------------------------------
/example/windows/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # Project-level configuration.
2 | cmake_minimum_required(VERSION 3.14)
3 | project(example LANGUAGES CXX)
4 |
5 | # The name of the executable created for the application. Change this to change
6 | # the on-disk name of your application.
7 | set(BINARY_NAME "example")
8 |
9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
10 | # versions of CMake.
11 | cmake_policy(VERSION 3.14...3.25)
12 |
13 | # Define build configuration option.
14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
15 | if(IS_MULTICONFIG)
16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
17 | CACHE STRING "" FORCE)
18 | else()
19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
20 | set(CMAKE_BUILD_TYPE "Debug" CACHE
21 | STRING "Flutter build mode" FORCE)
22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
23 | "Debug" "Profile" "Release")
24 | endif()
25 | endif()
26 | # Define settings for the Profile build mode.
27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
31 |
32 | # Use Unicode for all projects.
33 | add_definitions(-DUNICODE -D_UNICODE)
34 |
35 | # Compilation settings that should be applied to most targets.
36 | #
37 | # Be cautious about adding new options here, as plugins use this function by
38 | # default. In most cases, you should add new options to specific targets instead
39 | # of modifying this function.
40 | function(APPLY_STANDARD_SETTINGS TARGET)
41 | target_compile_features(${TARGET} PUBLIC cxx_std_17)
42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
43 | target_compile_options(${TARGET} PRIVATE /EHsc)
44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>")
46 | endfunction()
47 |
48 | # Flutter library and tool build rules.
49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
50 | add_subdirectory(${FLUTTER_MANAGED_DIR})
51 |
52 | # Application build; see runner/CMakeLists.txt.
53 | add_subdirectory("runner")
54 |
55 |
56 | # Generated plugin build rules, which manage building the plugins and adding
57 | # them to the application.
58 | include(flutter/generated_plugins.cmake)
59 |
60 |
61 | # === Installation ===
62 | # Support files are copied into place next to the executable, so that it can
63 | # run in place. This is done instead of making a separate bundle (as on Linux)
64 | # so that building and running from within Visual Studio will work.
65 | set(BUILD_BUNDLE_DIR "$")
66 | # Make the "install" step default, as it's required to run.
67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
70 | endif()
71 |
72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
74 |
75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
76 | COMPONENT Runtime)
77 |
78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
79 | COMPONENT Runtime)
80 |
81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
82 | COMPONENT Runtime)
83 |
84 | if(PLUGIN_BUNDLED_LIBRARIES)
85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
87 | COMPONENT Runtime)
88 | endif()
89 |
90 | # Copy the native assets provided by the build.dart from all packages.
91 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
92 | install(DIRECTORY "${NATIVE_ASSETS_DIR}"
93 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
94 | COMPONENT Runtime)
95 |
96 | # Fully re-copy the assets directory on each build to avoid having stale files
97 | # from a previous install.
98 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
99 | install(CODE "
100 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
101 | " COMPONENT Runtime)
102 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
103 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
104 |
105 | # Install the AOT library on non-Debug builds only.
106 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
107 | CONFIGURATIONS Profile;Release
108 | COMPONENT Runtime)
109 |
--------------------------------------------------------------------------------
/example/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: example
2 | description: "A new Flutter project."
3 | # The following line prevents the package from being accidentally published to
4 | # pub.dev using `flutter pub publish`. This is preferred for private packages.
5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev
6 |
7 | # The following defines the version and build number for your application.
8 | # A version number is three numbers separated by dots, like 1.2.43
9 | # followed by an optional build number separated by a +.
10 | # Both the version and the builder number may be overridden in flutter
11 | # build by specifying --build-name and --build-number, respectively.
12 | # In Android, build-name is used as versionName while build-number used as versionCode.
13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning
14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
15 | # Read more about iOS versioning at
16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17 | # In Windows, build-name is used as the major, minor, and patch parts
18 | # of the product and file versions while build-number is used as the build suffix.
19 | version: 1.0.0+1
20 |
21 | environment:
22 | sdk: '>=3.4.3 <4.0.0'
23 |
24 | # Dependencies specify other packages that your package needs in order to work.
25 | # To automatically upgrade your package dependencies to the latest versions
26 | # consider running `flutter pub upgrade --major-versions`. Alternatively,
27 | # dependencies can be manually updated by changing the version numbers below to
28 | # the latest version available on pub.dev. To see which dependencies have newer
29 | # versions available, run `flutter pub outdated`.
30 | dependencies:
31 | # The following adds the Cupertino Icons font to your application.
32 | # Use with the CupertinoIcons class for iOS style icons.
33 | cupertino_icons: ^1.0.8
34 | faker: ^2.1.0
35 | file_picker: ^8.0.3
36 | firebase_auth: ^5.0.0
37 | firebase_core: ^3.0.0
38 | firebase_storage: ^12.0.0
39 | flutter:
40 | sdk: flutter
41 | flutter_chat_types: ^3.6.2
42 | flutter_chat_ui: ^1.6.14
43 | flutter_firebase_chat_core:
44 | path: ../
45 | http: '>=0.13.6 <2.0.0'
46 | image_picker: ^1.1.2
47 | mime: ^1.0.5
48 | open_filex: ^4.4.0
49 | path_provider: ^2.1.3
50 |
51 | dev_dependencies:
52 | dart_code_metrics: ^5.7.6
53 | # The "flutter_lints" package below contains a set of recommended lints to
54 | # encourage good coding practices. The lint set provided by the package is
55 | # activated in the `analysis_options.yaml` file located at the root of your
56 | # package. See that file for information about deactivating specific lint
57 | # rules and activating additional ones.
58 | flutter_lints: ^4.0.0
59 | flutter_test:
60 | sdk: flutter
61 |
62 | # For information on the generic Dart part of this file, see the
63 | # following page: https://dart.dev/tools/pub/pubspec
64 |
65 | # The following section is specific to Flutter packages.
66 | flutter:
67 |
68 | # The following line ensures that the Material Icons font is
69 | # included with your application, so that you can use the icons in
70 | # the material Icons class.
71 | uses-material-design: true
72 |
73 | # To add assets to your application, add an assets section, like this:
74 | # assets:
75 | # - images/a_dot_burr.jpeg
76 | # - images/a_dot_ham.jpeg
77 |
78 | # An image asset can refer to one or more resolution-specific "variants", see
79 | # https://flutter.dev/assets-and-images/#resolution-aware
80 |
81 | # For details regarding adding assets from package dependencies, see
82 | # https://flutter.dev/assets-and-images/#from-packages
83 |
84 | # To add custom fonts to your application, add a fonts section here,
85 | # in this "flutter" section. Each entry in this list should have a
86 | # "family" key with the font family name, and a "fonts" key with a
87 | # list giving the asset and other descriptors for the font. For
88 | # example:
89 | # fonts:
90 | # - family: Schyler
91 | # fonts:
92 | # - asset: fonts/Schyler-Regular.ttf
93 | # - asset: fonts/Schyler-Italic.ttf
94 | # style: italic
95 | # - family: Trajan Pro
96 | # fonts:
97 | # - asset: fonts/TrajanPro.ttf
98 | # - asset: fonts/TrajanPro_Bold.ttf
99 | # weight: 700
100 | #
101 | # For details regarding fonts from package dependencies,
102 | # see https://flutter.dev/custom-fonts/#from-packages
103 |
--------------------------------------------------------------------------------
/example/linux/my_application.cc:
--------------------------------------------------------------------------------
1 | #include "my_application.h"
2 |
3 | #include
4 | #ifdef GDK_WINDOWING_X11
5 | #include
6 | #endif
7 |
8 | #include "flutter/generated_plugin_registrant.h"
9 |
10 | struct _MyApplication {
11 | GtkApplication parent_instance;
12 | char** dart_entrypoint_arguments;
13 | };
14 |
15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
16 |
17 | // Implements GApplication::activate.
18 | static void my_application_activate(GApplication* application) {
19 | MyApplication* self = MY_APPLICATION(application);
20 | GtkWindow* window =
21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
22 |
23 | // Use a header bar when running in GNOME as this is the common style used
24 | // by applications and is the setup most users will be using (e.g. Ubuntu
25 | // desktop).
26 | // If running on X and not using GNOME then just use a traditional title bar
27 | // in case the window manager does more exotic layout, e.g. tiling.
28 | // If running on Wayland assume the header bar will work (may need changing
29 | // if future cases occur).
30 | gboolean use_header_bar = TRUE;
31 | #ifdef GDK_WINDOWING_X11
32 | GdkScreen* screen = gtk_window_get_screen(window);
33 | if (GDK_IS_X11_SCREEN(screen)) {
34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
36 | use_header_bar = FALSE;
37 | }
38 | }
39 | #endif
40 | if (use_header_bar) {
41 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
42 | gtk_widget_show(GTK_WIDGET(header_bar));
43 | gtk_header_bar_set_title(header_bar, "example");
44 | gtk_header_bar_set_show_close_button(header_bar, TRUE);
45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
46 | } else {
47 | gtk_window_set_title(window, "example");
48 | }
49 |
50 | gtk_window_set_default_size(window, 1280, 720);
51 | gtk_widget_show(GTK_WIDGET(window));
52 |
53 | g_autoptr(FlDartProject) project = fl_dart_project_new();
54 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
55 |
56 | FlView* view = fl_view_new(project);
57 | gtk_widget_show(GTK_WIDGET(view));
58 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
59 |
60 | fl_register_plugins(FL_PLUGIN_REGISTRY(view));
61 |
62 | gtk_widget_grab_focus(GTK_WIDGET(view));
63 | }
64 |
65 | // Implements GApplication::local_command_line.
66 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
67 | MyApplication* self = MY_APPLICATION(application);
68 | // Strip out the first argument as it is the binary name.
69 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
70 |
71 | g_autoptr(GError) error = nullptr;
72 | if (!g_application_register(application, nullptr, &error)) {
73 | g_warning("Failed to register: %s", error->message);
74 | *exit_status = 1;
75 | return TRUE;
76 | }
77 |
78 | g_application_activate(application);
79 | *exit_status = 0;
80 |
81 | return TRUE;
82 | }
83 |
84 | // Implements GApplication::startup.
85 | static void my_application_startup(GApplication* application) {
86 | //MyApplication* self = MY_APPLICATION(object);
87 |
88 | // Perform any actions required at application startup.
89 |
90 | G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
91 | }
92 |
93 | // Implements GApplication::shutdown.
94 | static void my_application_shutdown(GApplication* application) {
95 | //MyApplication* self = MY_APPLICATION(object);
96 |
97 | // Perform any actions required at application shutdown.
98 |
99 | G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
100 | }
101 |
102 | // Implements GObject::dispose.
103 | static void my_application_dispose(GObject* object) {
104 | MyApplication* self = MY_APPLICATION(object);
105 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
106 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
107 | }
108 |
109 | static void my_application_class_init(MyApplicationClass* klass) {
110 | G_APPLICATION_CLASS(klass)->activate = my_application_activate;
111 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
112 | G_APPLICATION_CLASS(klass)->startup = my_application_startup;
113 | G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
114 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
115 | }
116 |
117 | static void my_application_init(MyApplication* self) {}
118 |
119 | MyApplication* my_application_new() {
120 | return MY_APPLICATION(g_object_new(my_application_get_type(),
121 | "application-id", APPLICATION_ID,
122 | "flags", G_APPLICATION_NON_UNIQUE,
123 | nullptr));
124 | }
125 |
--------------------------------------------------------------------------------
/example/lib/login.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_auth/firebase_auth.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter/services.dart';
4 |
5 | import 'register.dart';
6 |
7 | class LoginPage extends StatefulWidget {
8 | const LoginPage({super.key});
9 |
10 | @override
11 | State createState() => _LoginPageState();
12 | }
13 |
14 | class _LoginPageState extends State {
15 | FocusNode? _focusNode;
16 | bool _loggingIn = false;
17 | TextEditingController? _passwordController;
18 | TextEditingController? _usernameController;
19 |
20 | @override
21 | void initState() {
22 | super.initState();
23 | _focusNode = FocusNode();
24 | _passwordController = TextEditingController(text: 'Qawsed1-');
25 | _usernameController = TextEditingController(text: '');
26 | }
27 |
28 | void _login() async {
29 | FocusScope.of(context).unfocus();
30 |
31 | setState(() {
32 | _loggingIn = true;
33 | });
34 |
35 | try {
36 | await FirebaseAuth.instance.signInWithEmailAndPassword(
37 | email: _usernameController!.text,
38 | password: _passwordController!.text,
39 | );
40 | if (!mounted) return;
41 | Navigator.of(context).pop();
42 | } catch (e) {
43 | setState(() {
44 | _loggingIn = false;
45 | });
46 |
47 | await showDialog(
48 | context: context,
49 | builder: (context) => AlertDialog(
50 | actions: [
51 | TextButton(
52 | onPressed: () {
53 | Navigator.of(context).pop();
54 | },
55 | child: const Text('OK'),
56 | ),
57 | ],
58 | content: Text(
59 | e.toString(),
60 | ),
61 | title: const Text('Error'),
62 | ),
63 | );
64 | }
65 | }
66 |
67 | @override
68 | void dispose() {
69 | _focusNode?.dispose();
70 | _passwordController?.dispose();
71 | _usernameController?.dispose();
72 | super.dispose();
73 | }
74 |
75 | @override
76 | Widget build(BuildContext context) => Scaffold(
77 | appBar: AppBar(
78 | systemOverlayStyle: SystemUiOverlayStyle.light,
79 | title: const Text('Login'),
80 | ),
81 | body: SingleChildScrollView(
82 | child: Container(
83 | padding: const EdgeInsets.only(top: 80, left: 24, right: 24),
84 | child: Column(
85 | children: [
86 | TextField(
87 | autocorrect: false,
88 | autofillHints: _loggingIn ? null : [AutofillHints.email],
89 | autofocus: true,
90 | controller: _usernameController,
91 | decoration: InputDecoration(
92 | border: const OutlineInputBorder(
93 | borderRadius: BorderRadius.all(
94 | Radius.circular(8),
95 | ),
96 | ),
97 | labelText: 'Email',
98 | suffixIcon: IconButton(
99 | icon: const Icon(Icons.cancel),
100 | onPressed: () => _usernameController?.clear(),
101 | ),
102 | ),
103 | keyboardType: TextInputType.emailAddress,
104 | onEditingComplete: () {
105 | _focusNode?.requestFocus();
106 | },
107 | readOnly: _loggingIn,
108 | textCapitalization: TextCapitalization.none,
109 | textInputAction: TextInputAction.next,
110 | ),
111 | Container(
112 | margin: const EdgeInsets.symmetric(vertical: 8),
113 | child: TextField(
114 | autocorrect: false,
115 | autofillHints: _loggingIn ? null : [AutofillHints.password],
116 | controller: _passwordController,
117 | decoration: InputDecoration(
118 | border: const OutlineInputBorder(
119 | borderRadius: BorderRadius.all(
120 | Radius.circular(8),
121 | ),
122 | ),
123 | labelText: 'Password',
124 | suffixIcon: IconButton(
125 | icon: const Icon(Icons.cancel),
126 | onPressed: () => _passwordController?.clear(),
127 | ),
128 | ),
129 | focusNode: _focusNode,
130 | keyboardType: TextInputType.emailAddress,
131 | obscureText: true,
132 | onEditingComplete: _login,
133 | textCapitalization: TextCapitalization.none,
134 | textInputAction: TextInputAction.done,
135 | ),
136 | ),
137 | TextButton(
138 | onPressed: _loggingIn ? null : _login,
139 | child: const Text('Login'),
140 | ),
141 | TextButton(
142 | onPressed: _loggingIn
143 | ? null
144 | : () {
145 | Navigator.of(context).push(
146 | MaterialPageRoute(
147 | builder: (context) => const RegisterPage(),
148 | ),
149 | );
150 | },
151 | child: const Text('Register'),
152 | ),
153 | ],
154 | ),
155 | ),
156 | ),
157 | );
158 | }
159 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, caste, color, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at .
63 | All complaints will be reviewed and investigated promptly and fairly.
64 |
65 | All community leaders are obligated to respect the privacy and security of the
66 | reporter of any incident.
67 |
68 | ## Enforcement Guidelines
69 |
70 | Community leaders will follow these Community Impact Guidelines in determining
71 | the consequences for any action they deem in violation of this Code of Conduct:
72 |
73 | ### 1. Correction
74 |
75 | **Community Impact**: Use of inappropriate language or other behavior deemed
76 | unprofessional or unwelcome in the community.
77 |
78 | **Consequence**: A private, written warning from community leaders, providing
79 | clarity around the nature of the violation and an explanation of why the
80 | behavior was inappropriate. A public apology may be requested.
81 |
82 | ### 2. Warning
83 |
84 | **Community Impact**: A violation through a single incident or series
85 | of actions.
86 |
87 | **Consequence**: A warning with consequences for continued behavior. No
88 | interaction with the people involved, including unsolicited interaction with
89 | those enforcing the Code of Conduct, for a specified period of time. This
90 | includes avoiding interactions in community spaces as well as external channels
91 | like social media. Violating these terms may lead to a temporary or
92 | permanent ban.
93 |
94 | ### 3. Temporary Ban
95 |
96 | **Community Impact**: A serious violation of community standards, including
97 | sustained inappropriate behavior.
98 |
99 | **Consequence**: A temporary ban from any sort of interaction or public
100 | communication with the community for a specified period of time. No public or
101 | private interaction with the people involved, including unsolicited interaction
102 | with those enforcing the Code of Conduct, is allowed during this period.
103 | Violating these terms may lead to a permanent ban.
104 |
105 | ### 4. Permanent Ban
106 |
107 | **Community Impact**: Demonstrating a pattern of violation of community
108 | standards, including sustained inappropriate behavior, harassment of an
109 | individual, or aggression toward or disparagement of classes of individuals.
110 |
111 | **Consequence**: A permanent ban from any sort of public interaction within
112 | the community.
113 |
114 | ## Attribution
115 |
116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
117 | version 2.0, available at
118 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
119 |
120 | Community Impact Guidelines were inspired by
121 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
122 |
123 | For answers to common questions about this code of conduct, see the FAQ at
124 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available
125 | at [https://www.contributor-covenant.org/translations][translations].
126 |
127 | [homepage]: https://www.contributor-covenant.org
128 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
129 | [Mozilla CoC]: https://github.com/mozilla/diversity
130 | [FAQ]: https://www.contributor-covenant.org/faq
131 | [translations]: https://www.contributor-covenant.org/translations
132 |
--------------------------------------------------------------------------------
/example/linux/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # Project-level configuration.
2 | cmake_minimum_required(VERSION 3.10)
3 | project(runner LANGUAGES CXX)
4 |
5 | # The name of the executable created for the application. Change this to change
6 | # the on-disk name of your application.
7 | set(BINARY_NAME "example")
8 | # The unique GTK application identifier for this application. See:
9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID
10 | set(APPLICATION_ID "com.example")
11 |
12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
13 | # versions of CMake.
14 | cmake_policy(SET CMP0063 NEW)
15 |
16 | # Load bundled libraries from the lib/ directory relative to the binary.
17 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
18 |
19 | # Root filesystem for cross-building.
20 | if(FLUTTER_TARGET_PLATFORM_SYSROOT)
21 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
22 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
25 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
26 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
27 | endif()
28 |
29 | # Define build configuration options.
30 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
31 | set(CMAKE_BUILD_TYPE "Debug" CACHE
32 | STRING "Flutter build mode" FORCE)
33 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
34 | "Debug" "Profile" "Release")
35 | endif()
36 |
37 | # Compilation settings that should be applied to most targets.
38 | #
39 | # Be cautious about adding new options here, as plugins use this function by
40 | # default. In most cases, you should add new options to specific targets instead
41 | # of modifying this function.
42 | function(APPLY_STANDARD_SETTINGS TARGET)
43 | target_compile_features(${TARGET} PUBLIC cxx_std_14)
44 | target_compile_options(${TARGET} PRIVATE -Wall -Werror)
45 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>")
46 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>")
47 | endfunction()
48 |
49 | # Flutter library and tool build rules.
50 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
51 | add_subdirectory(${FLUTTER_MANAGED_DIR})
52 |
53 | # System-level dependencies.
54 | find_package(PkgConfig REQUIRED)
55 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
56 |
57 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
58 |
59 | # Define the application target. To change its name, change BINARY_NAME above,
60 | # not the value here, or `flutter run` will no longer work.
61 | #
62 | # Any new source files that you add to the application should be added here.
63 | add_executable(${BINARY_NAME}
64 | "main.cc"
65 | "my_application.cc"
66 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
67 | )
68 |
69 | # Apply the standard set of build settings. This can be removed for applications
70 | # that need different build settings.
71 | apply_standard_settings(${BINARY_NAME})
72 |
73 | # Add dependency libraries. Add any application-specific dependencies here.
74 | target_link_libraries(${BINARY_NAME} PRIVATE flutter)
75 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
76 |
77 | # Run the Flutter tool portions of the build. This must not be removed.
78 | add_dependencies(${BINARY_NAME} flutter_assemble)
79 |
80 | # Only the install-generated bundle's copy of the executable will launch
81 | # correctly, since the resources must in the right relative locations. To avoid
82 | # people trying to run the unbundled copy, put it in a subdirectory instead of
83 | # the default top-level location.
84 | set_target_properties(${BINARY_NAME}
85 | PROPERTIES
86 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
87 | )
88 |
89 |
90 | # Generated plugin build rules, which manage building the plugins and adding
91 | # them to the application.
92 | include(flutter/generated_plugins.cmake)
93 |
94 |
95 | # === Installation ===
96 | # By default, "installing" just makes a relocatable bundle in the build
97 | # directory.
98 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
99 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
100 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
101 | endif()
102 |
103 | # Start with a clean build bundle directory every time.
104 | install(CODE "
105 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
106 | " COMPONENT Runtime)
107 |
108 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
109 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
110 |
111 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
112 | COMPONENT Runtime)
113 |
114 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
115 | COMPONENT Runtime)
116 |
117 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
118 | COMPONENT Runtime)
119 |
120 | foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
121 | install(FILES "${bundled_library}"
122 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
123 | COMPONENT Runtime)
124 | endforeach(bundled_library)
125 |
126 | # Copy the native assets provided by the build.dart from all packages.
127 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
128 | install(DIRECTORY "${NATIVE_ASSETS_DIR}"
129 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
130 | COMPONENT Runtime)
131 |
132 | # Fully re-copy the assets directory on each build to avoid having stale files
133 | # from a previous install.
134 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
135 | install(CODE "
136 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
137 | " COMPONENT Runtime)
138 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
139 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
140 |
141 | # Install the AOT library on non-Debug builds only.
142 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
143 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
144 | COMPONENT Runtime)
145 | endif()
146 |
--------------------------------------------------------------------------------
/example/lib/register.dart:
--------------------------------------------------------------------------------
1 | import 'package:faker/faker.dart';
2 | import 'package:firebase_auth/firebase_auth.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter/services.dart';
5 | import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
6 | import 'package:flutter_firebase_chat_core/flutter_firebase_chat_core.dart';
7 |
8 | class RegisterPage extends StatefulWidget {
9 | const RegisterPage({super.key});
10 |
11 | @override
12 | State createState() => _RegisterPageState();
13 | }
14 |
15 | class _RegisterPageState extends State {
16 | String? _email;
17 | String? _firstName;
18 | FocusNode? _focusNode;
19 | String? _lastName;
20 | TextEditingController? _passwordController;
21 | bool _registering = false;
22 | TextEditingController? _usernameController;
23 |
24 | @override
25 | void initState() {
26 | super.initState();
27 | final faker = Faker();
28 | _firstName = faker.person.firstName();
29 | _lastName = faker.person.lastName();
30 | _email =
31 | '${_firstName!.toLowerCase()}.${_lastName!.toLowerCase()}@${faker.internet.domainName()}';
32 | _focusNode = FocusNode();
33 | _passwordController = TextEditingController(text: 'Qawsed1-');
34 | _usernameController = TextEditingController(
35 | text: _email,
36 | );
37 | }
38 |
39 | void _register() async {
40 | FocusScope.of(context).unfocus();
41 |
42 | setState(() {
43 | _registering = true;
44 | });
45 |
46 | try {
47 | final credential =
48 | await FirebaseAuth.instance.createUserWithEmailAndPassword(
49 | email: _usernameController!.text,
50 | password: _passwordController!.text,
51 | );
52 | await FirebaseChatCore.instance.createUserInFirestore(
53 | types.User(
54 | firstName: _firstName,
55 | id: credential.user!.uid,
56 | imageUrl: 'https://i.pravatar.cc/300?u=$_email',
57 | lastName: _lastName,
58 | ),
59 | );
60 |
61 | if (!mounted) return;
62 | Navigator.of(context)
63 | ..pop()
64 | ..pop();
65 | } catch (e) {
66 | setState(() {
67 | _registering = false;
68 | });
69 |
70 | await showDialog(
71 | context: context,
72 | builder: (context) => AlertDialog(
73 | actions: [
74 | TextButton(
75 | onPressed: () {
76 | Navigator.of(context).pop();
77 | },
78 | child: const Text('OK'),
79 | ),
80 | ],
81 | content: Text(
82 | e.toString(),
83 | ),
84 | title: const Text('Error'),
85 | ),
86 | );
87 | }
88 | }
89 |
90 | @override
91 | void dispose() {
92 | _focusNode?.dispose();
93 | _passwordController?.dispose();
94 | _usernameController?.dispose();
95 | super.dispose();
96 | }
97 |
98 | @override
99 | Widget build(BuildContext context) => Scaffold(
100 | appBar: AppBar(
101 | systemOverlayStyle: SystemUiOverlayStyle.light,
102 | title: const Text('Register'),
103 | ),
104 | body: SingleChildScrollView(
105 | child: Container(
106 | padding: const EdgeInsets.only(top: 80, left: 24, right: 24),
107 | child: Column(
108 | children: [
109 | TextField(
110 | autocorrect: false,
111 | autofillHints: _registering ? null : [AutofillHints.email],
112 | autofocus: true,
113 | controller: _usernameController,
114 | decoration: InputDecoration(
115 | border: const OutlineInputBorder(
116 | borderRadius: BorderRadius.all(
117 | Radius.circular(8),
118 | ),
119 | ),
120 | labelText: 'Email',
121 | suffixIcon: IconButton(
122 | icon: const Icon(Icons.cancel),
123 | onPressed: () => _usernameController?.clear(),
124 | ),
125 | ),
126 | keyboardType: TextInputType.emailAddress,
127 | onEditingComplete: () {
128 | _focusNode?.requestFocus();
129 | },
130 | readOnly: _registering,
131 | textCapitalization: TextCapitalization.none,
132 | textInputAction: TextInputAction.next,
133 | ),
134 | Container(
135 | margin: const EdgeInsets.symmetric(vertical: 8),
136 | child: TextField(
137 | autocorrect: false,
138 | autofillHints:
139 | _registering ? null : [AutofillHints.password],
140 | controller: _passwordController,
141 | decoration: InputDecoration(
142 | border: const OutlineInputBorder(
143 | borderRadius: BorderRadius.all(
144 | Radius.circular(8),
145 | ),
146 | ),
147 | labelText: 'Password',
148 | suffixIcon: IconButton(
149 | icon: const Icon(Icons.cancel),
150 | onPressed: () => _passwordController?.clear(),
151 | ),
152 | ),
153 | focusNode: _focusNode,
154 | keyboardType: TextInputType.emailAddress,
155 | obscureText: true,
156 | onEditingComplete: _register,
157 | textCapitalization: TextCapitalization.none,
158 | textInputAction: TextInputAction.done,
159 | ),
160 | ),
161 | TextButton(
162 | onPressed: _registering ? null : _register,
163 | child: const Text('Register'),
164 | ),
165 | ],
166 | ),
167 | ),
168 | ),
169 | );
170 | }
171 |
--------------------------------------------------------------------------------
/example/lib/rooms.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_auth/firebase_auth.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter/services.dart';
4 | import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
5 | import 'package:flutter_firebase_chat_core/flutter_firebase_chat_core.dart';
6 |
7 | import 'chat.dart';
8 | import 'login.dart';
9 | import 'users.dart';
10 | import 'util.dart';
11 |
12 | class RoomsPage extends StatefulWidget {
13 | const RoomsPage({super.key});
14 |
15 | @override
16 | State createState() => _RoomsPageState();
17 | }
18 |
19 | class _RoomsPageState extends State {
20 | bool _error = false;
21 | bool _initialized = false;
22 | User? _user;
23 |
24 | @override
25 | void initState() {
26 | initializeFlutterFire();
27 | super.initState();
28 | }
29 |
30 | void initializeFlutterFire() async {
31 | try {
32 | FirebaseAuth.instance.authStateChanges().listen((User? user) {
33 | setState(() {
34 | _user = user;
35 | });
36 | });
37 | setState(() {
38 | _initialized = true;
39 | });
40 | } catch (e) {
41 | setState(() {
42 | _error = true;
43 | });
44 | }
45 | }
46 |
47 | void logout() async {
48 | await FirebaseAuth.instance.signOut();
49 | }
50 |
51 | Widget _buildAvatar(types.Room room) {
52 | var color = Colors.transparent;
53 |
54 | if (room.type == types.RoomType.direct) {
55 | try {
56 | final otherUser = room.users.firstWhere(
57 | (u) => u.id != _user!.uid,
58 | );
59 |
60 | color = getUserAvatarNameColor(otherUser);
61 | } catch (e) {
62 | // Do nothing if other user is not found.
63 | }
64 | }
65 |
66 | final hasImage = room.imageUrl != null;
67 | final name = room.name ?? '';
68 |
69 | return Container(
70 | margin: const EdgeInsets.only(right: 16),
71 | child: CircleAvatar(
72 | backgroundColor: hasImage ? Colors.transparent : color,
73 | backgroundImage: hasImage ? NetworkImage(room.imageUrl!) : null,
74 | radius: 20,
75 | child: !hasImage
76 | ? Text(
77 | name.isEmpty ? '' : name[0].toUpperCase(),
78 | style: const TextStyle(color: Colors.white),
79 | )
80 | : null,
81 | ),
82 | );
83 | }
84 |
85 | @override
86 | Widget build(BuildContext context) {
87 | if (_error) {
88 | return Container();
89 | }
90 |
91 | if (!_initialized) {
92 | return Container();
93 | }
94 |
95 | return Scaffold(
96 | appBar: AppBar(
97 | actions: [
98 | IconButton(
99 | icon: const Icon(Icons.add),
100 | onPressed: _user == null
101 | ? null
102 | : () {
103 | Navigator.of(context).push(
104 | MaterialPageRoute(
105 | fullscreenDialog: true,
106 | builder: (context) => const UsersPage(),
107 | ),
108 | );
109 | },
110 | ),
111 | ],
112 | leading: IconButton(
113 | icon: const Icon(Icons.logout),
114 | onPressed: _user == null ? null : logout,
115 | ),
116 | systemOverlayStyle: SystemUiOverlayStyle.light,
117 | title: const Text('Rooms'),
118 | ),
119 | body: _user == null
120 | ? Container(
121 | alignment: Alignment.center,
122 | margin: const EdgeInsets.only(
123 | bottom: 200,
124 | ),
125 | child: Column(
126 | mainAxisAlignment: MainAxisAlignment.center,
127 | children: [
128 | const Text('Not authenticated'),
129 | TextButton(
130 | onPressed: () {
131 | Navigator.of(context).push(
132 | MaterialPageRoute(
133 | fullscreenDialog: true,
134 | builder: (context) => const LoginPage(),
135 | ),
136 | );
137 | },
138 | child: const Text('Login'),
139 | ),
140 | ],
141 | ),
142 | )
143 | : StreamBuilder>(
144 | stream: FirebaseChatCore.instance.rooms(),
145 | initialData: const [],
146 | builder: (context, snapshot) {
147 | if (!snapshot.hasData || snapshot.data!.isEmpty) {
148 | return Container(
149 | alignment: Alignment.center,
150 | margin: const EdgeInsets.only(
151 | bottom: 200,
152 | ),
153 | child: const Text('No rooms'),
154 | );
155 | }
156 |
157 | return ListView.builder(
158 | itemCount: snapshot.data!.length,
159 | itemBuilder: (context, index) {
160 | final room = snapshot.data![index];
161 |
162 | return GestureDetector(
163 | onTap: () {
164 | Navigator.of(context).push(
165 | MaterialPageRoute(
166 | builder: (context) => ChatPage(
167 | room: room,
168 | ),
169 | ),
170 | );
171 | },
172 | child: Container(
173 | padding: const EdgeInsets.symmetric(
174 | horizontal: 16,
175 | vertical: 8,
176 | ),
177 | child: Row(
178 | children: [
179 | _buildAvatar(room),
180 | Text(room.name ?? ''),
181 | ],
182 | ),
183 | ),
184 | );
185 | },
186 | );
187 | },
188 | ),
189 | );
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/example/lib/chat.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:file_picker/file_picker.dart';
4 | import 'package:firebase_storage/firebase_storage.dart';
5 | import 'package:flutter/material.dart';
6 | import 'package:flutter/services.dart';
7 | import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
8 | import 'package:flutter_chat_ui/flutter_chat_ui.dart';
9 | import 'package:flutter_firebase_chat_core/flutter_firebase_chat_core.dart';
10 | import 'package:http/http.dart' as http;
11 | import 'package:image_picker/image_picker.dart';
12 | import 'package:mime/mime.dart';
13 | import 'package:open_filex/open_filex.dart';
14 | import 'package:path_provider/path_provider.dart';
15 |
16 | class ChatPage extends StatefulWidget {
17 | const ChatPage({
18 | super.key,
19 | required this.room,
20 | });
21 |
22 | final types.Room room;
23 |
24 | @override
25 | State createState() => _ChatPageState();
26 | }
27 |
28 | class _ChatPageState extends State {
29 | bool _isAttachmentUploading = false;
30 |
31 | void _handleAtachmentPressed() {
32 | showModalBottomSheet(
33 | context: context,
34 | builder: (BuildContext context) => SafeArea(
35 | child: SizedBox(
36 | height: 144,
37 | child: Column(
38 | crossAxisAlignment: CrossAxisAlignment.stretch,
39 | children: [
40 | TextButton(
41 | onPressed: () {
42 | Navigator.pop(context);
43 | _handleImageSelection();
44 | },
45 | child: const Align(
46 | alignment: Alignment.centerLeft,
47 | child: Text('Photo'),
48 | ),
49 | ),
50 | TextButton(
51 | onPressed: () {
52 | Navigator.pop(context);
53 | _handleFileSelection();
54 | },
55 | child: const Align(
56 | alignment: Alignment.centerLeft,
57 | child: Text('File'),
58 | ),
59 | ),
60 | TextButton(
61 | onPressed: () => Navigator.pop(context),
62 | child: const Align(
63 | alignment: Alignment.centerLeft,
64 | child: Text('Cancel'),
65 | ),
66 | ),
67 | ],
68 | ),
69 | ),
70 | ),
71 | );
72 | }
73 |
74 | void _handleFileSelection() async {
75 | final result = await FilePicker.platform.pickFiles(
76 | type: FileType.any,
77 | );
78 |
79 | if (result != null && result.files.single.path != null) {
80 | _setAttachmentUploading(true);
81 | final name = result.files.single.name;
82 | final filePath = result.files.single.path!;
83 | final file = File(filePath);
84 |
85 | try {
86 | final reference = FirebaseStorage.instance.ref(name);
87 | await reference.putFile(file);
88 | final uri = await reference.getDownloadURL();
89 |
90 | final message = types.PartialFile(
91 | mimeType: lookupMimeType(filePath),
92 | name: name,
93 | size: result.files.single.size,
94 | uri: uri,
95 | );
96 |
97 | FirebaseChatCore.instance.sendMessage(message, widget.room.id);
98 | _setAttachmentUploading(false);
99 | } finally {
100 | _setAttachmentUploading(false);
101 | }
102 | }
103 | }
104 |
105 | void _handleImageSelection() async {
106 | final result = await ImagePicker().pickImage(
107 | imageQuality: 70,
108 | maxWidth: 1440,
109 | source: ImageSource.gallery,
110 | );
111 |
112 | if (result != null) {
113 | _setAttachmentUploading(true);
114 | final file = File(result.path);
115 | final size = file.lengthSync();
116 | final bytes = await result.readAsBytes();
117 | final image = await decodeImageFromList(bytes);
118 | final name = result.name;
119 |
120 | try {
121 | final reference = FirebaseStorage.instance.ref(name);
122 | await reference.putFile(file);
123 | final uri = await reference.getDownloadURL();
124 |
125 | final message = types.PartialImage(
126 | height: image.height.toDouble(),
127 | name: name,
128 | size: size,
129 | uri: uri,
130 | width: image.width.toDouble(),
131 | );
132 |
133 | FirebaseChatCore.instance.sendMessage(
134 | message,
135 | widget.room.id,
136 | );
137 | _setAttachmentUploading(false);
138 | } finally {
139 | _setAttachmentUploading(false);
140 | }
141 | }
142 | }
143 |
144 | void _handleMessageTap(BuildContext _, types.Message message) async {
145 | if (message is types.FileMessage) {
146 | var localPath = message.uri;
147 |
148 | if (message.uri.startsWith('http')) {
149 | try {
150 | final updatedMessage = message.copyWith(isLoading: true);
151 | FirebaseChatCore.instance.updateMessage(
152 | updatedMessage,
153 | widget.room.id,
154 | );
155 |
156 | final client = http.Client();
157 | final request = await client.get(Uri.parse(message.uri));
158 | final bytes = request.bodyBytes;
159 | final documentsDir = (await getApplicationDocumentsDirectory()).path;
160 | localPath = '$documentsDir/${message.name}';
161 |
162 | if (!File(localPath).existsSync()) {
163 | final file = File(localPath);
164 | await file.writeAsBytes(bytes);
165 | }
166 | } finally {
167 | final updatedMessage = message.copyWith(isLoading: false);
168 | FirebaseChatCore.instance.updateMessage(
169 | updatedMessage,
170 | widget.room.id,
171 | );
172 | }
173 | }
174 |
175 | await OpenFilex.open(localPath);
176 | }
177 | }
178 |
179 | void _handlePreviewDataFetched(
180 | types.TextMessage message,
181 | types.PreviewData previewData,
182 | ) {
183 | final updatedMessage = message.copyWith(previewData: previewData);
184 |
185 | FirebaseChatCore.instance.updateMessage(updatedMessage, widget.room.id);
186 | }
187 |
188 | void _handleSendPressed(types.PartialText message) {
189 | FirebaseChatCore.instance.sendMessage(
190 | message,
191 | widget.room.id,
192 | );
193 | }
194 |
195 | void _setAttachmentUploading(bool uploading) {
196 | setState(() {
197 | _isAttachmentUploading = uploading;
198 | });
199 | }
200 |
201 | @override
202 | Widget build(BuildContext context) => Scaffold(
203 | appBar: AppBar(
204 | systemOverlayStyle: SystemUiOverlayStyle.light,
205 | title: const Text('Chat'),
206 | ),
207 | body: StreamBuilder(
208 | initialData: widget.room,
209 | stream: FirebaseChatCore.instance.room(widget.room.id),
210 | builder: (context, snapshot) => StreamBuilder>(
211 | initialData: const [],
212 | stream: FirebaseChatCore.instance.messages(snapshot.data!),
213 | builder: (context, snapshot) => Chat(
214 | isAttachmentUploading: _isAttachmentUploading,
215 | messages: snapshot.data ?? [],
216 | onAttachmentPressed: _handleAtachmentPressed,
217 | onMessageTap: _handleMessageTap,
218 | onPreviewDataFetched: _handlePreviewDataFetched,
219 | onSendPressed: _handleSendPressed,
220 | user: types.User(
221 | id: FirebaseChatCore.instance.firebaseUser?.uid ?? '',
222 | ),
223 | ),
224 | ),
225 | ),
226 | );
227 | }
228 |
--------------------------------------------------------------------------------
/doc/firebase-usage.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: firebase-usage
3 | title: Usage
4 | ---
5 |
6 | As mentioned in [How it works?](firebase-overview#how-it-works), you will need to register a user using [Firebase Authentication](https://firebase.google.com/docs/auth). If you are using Firebase Authentication as your auth provider you don't need to do anything except calling `FirebaseChatCore.instance.createUserInFirestore` after registration.
7 |
8 | ```dart
9 | import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
10 | import 'package:flutter_firebase_chat_core/flutter_firebase_chat_core.dart';
11 |
12 | await FirebaseChatCore.instance.createUserInFirestore(
13 | types.User(
14 | firstName: 'John',
15 | id: credential.user!.uid, // UID from Firebase Authentication
16 | imageUrl: 'https://i.pravatar.cc/300',
17 | lastName: 'Doe',
18 | ),
19 | );
20 | ```
21 |
22 | You can provide values like `firstName`, `imageUrl` and `lastName` if you're planning to have a screen with all users available for chat. The `id` is the only required field and you **need to** use the `uid` you get from the Firebase Authentication after you register a user. If you don't use Firebase for authentication, you can register a user using your custom `JWT` token, then call `FirebaseChatCore.instance.createUserInFirestore` as described above.
23 |
24 | Aside from registration, you will need to log users in when appropriate, using available methods from Firebase Authentication, including the custom `JWT` token.
25 |
26 | ## Firebase Chat with custom backend
27 |
28 | This wasn't verified on production, but if you have your backend and want to use Firebase only for the chat functionality, you can register/login users using custom `JWT` token as described above and save received `uid` to your `users` table. Then you can have a screen with all users from your `users` table where each of them will have an assigned `uid` that will be used to start a chat. Or maybe you will have a search mechanism implemented on your backend, or you don't show users at all, just a button to start a chat with a random person, you still have access to that `uid`.
29 |
30 | Alternatively, you can use the `FirebaseChatCore.instance.users()` stream which will return all registered users with avatars and names.
31 |
32 | ```dart
33 | import 'package:flutter/material.dart';
34 | import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
35 | import 'package:flutter_firebase_chat_core/flutter_firebase_chat_core.dart';
36 |
37 | class UsersPage extends StatelessWidget {
38 | const UsersPage({Key? key}) : super(key: key);
39 |
40 | @override
41 | Widget build(BuildContext context) {
42 | return Scaffold(
43 | body: StreamBuilder>(
44 | stream: FirebaseChatCore.instance.users(),
45 | initialData: const [],
46 | builder: (context, snapshot) {
47 | // ...
48 | },
49 | ),
50 | );
51 | }
52 | }
53 | ```
54 |
55 | ## Starting a chat
56 |
57 | When you have access to that `uid` or you have the whole `User` class from the `FirebaseChatCore.instance.users()` stream, you can call either `createRoom` or `createGroupRoom`. For the group, you will need to additionally provide a name and an optional image.
58 |
59 | ```dart
60 | import 'package:flutter/material.dart';
61 | import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
62 | import 'package:flutter_firebase_chat_core/flutter_firebase_chat_core.dart';
63 |
64 | class UsersPage extends StatelessWidget {
65 | const UsersPage({Key? key}) : super(key: key);
66 |
67 | // Create a user with an ID of UID if you don't use `FirebaseChatCore.instance.users()` stream
68 | void _handlePressed(types.User otherUser, BuildContext context) async {
69 | final room = await FirebaseChatCore.instance.createRoom(otherUser);
70 |
71 | // Navigate to the Chat screen
72 | }
73 |
74 | @override
75 | Widget build(BuildContext context) {
76 | return Scaffold(
77 | body: StreamBuilder>(
78 | stream: FirebaseChatCore.instance.users(),
79 | initialData: const [],
80 | builder: (context, snapshot) {
81 | // ...
82 | },
83 | ),
84 | );
85 | }
86 | }
87 | ```
88 |
89 | ## Rooms
90 |
91 | To render user's rooms you use the `FirebaseChatCore.instance.rooms()` stream. `Room` class will have the name and image URL taken either from provided ones for the group or set to the other person's image URL and name. See [Security Rules](firebase-rules) for more info about rooms filtering.
92 |
93 | ```dart
94 | import 'package:flutter/material.dart';
95 | import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
96 | import 'package:flutter_firebase_chat_core/flutter_firebase_chat_core.dart';
97 |
98 | class RoomsPage extends StatelessWidget {
99 | const RoomsPage({Key? key}) : super(key: key);
100 |
101 | @override
102 | Widget build(BuildContext context) {
103 | return Scaffold(
104 | body: StreamBuilder>(
105 | stream: FirebaseChatCore.instance.rooms(),
106 | initialData: const [],
107 | builder: (context, snapshot) {
108 | // ...
109 | },
110 | ),
111 | );
112 | }
113 | }
114 | ```
115 |
116 | ## Messages
117 |
118 | `FirebaseChatCore.instance.messages` stream will give you access to all messages in the specified room. If you want to have dynamic updates for the room itself, you will need to wrap messages stream with a room stream. See the [example](https://github.com/flyerhq/flutter_firebase_chat_core/blob/main/example/lib/chat.dart).
119 |
120 | ```dart
121 | import 'package:flutter/material.dart';
122 | import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
123 | import 'package:flutter_firebase_chat_core/flutter_firebase_chat_core.dart';
124 |
125 | class ChatPage extends StatelessWidget {
126 | const ChatPage({Key? key}) : super(key: key);
127 |
128 | @override
129 | Widget build(BuildContext context) {
130 | return Scaffold(
131 | body: StreamBuilder>(
132 | initialData: const [],
133 | stream: FirebaseChatCore.instance.messages(widget.room),
134 | builder: (context, snapshot) {
135 | // ...
136 | },
137 | ),
138 | );
139 | }
140 | }
141 | ```
142 |
143 | If you use Flyer Chat UI you can just pass `snapshot.data ?? []` to the `messages` parameter of the Chat widget. See the [example](https://github.com/flyerhq/flutter_firebase_chat_core/blob/main/example/lib/chat.dart).
144 |
145 | ### Send a message
146 |
147 | To send a message use `FirebaseChatCore.instance.sendMessage`, it accepts 2 parameters:
148 |
149 | * Any partial message. Click [here](/chat-ui/types) to learn more about the types or check the [API reference](https://pub.dev/documentation/flutter_chat_types/latest/index.html). You provide a partial message because Firebase will set fields like `authorId`, `createdAt` and `id` automatically.
150 | * Room ID.
151 |
152 | ### Update the message
153 |
154 | To update the message use `FirebaseChatCore.instance.updateMessage`, it accepts 2 parameters:
155 |
156 | * Any message. Click [here](/chat-ui/types) to learn more about the types or check the [API reference](https://pub.dev/documentation/flutter_chat_types/latest/index.html). Use a message you get from the `FirebaseChatCore.instance.messages` stream, update it and send as this parameter.
157 | * Room ID.
158 |
159 | ## `firebaseUser`
160 |
161 | `FirebaseChatCore.instance.firebaseUser` is a shortcut you can use to see which user is currently logged in through Firebase Authentication. The returned type comes from the Firebase library and **it is not the same `User` as from the `flutter_chat_types` package**.
162 |
163 | :::note
164 |
165 | `firebaseUser` will not be updated dynamically, if you are looking for that feature, use `FirebaseAuth.instance.authStateChanges()` from the [Authentication](https://firebase.flutter.dev/docs/auth/overview) plugin.
166 |
167 | :::
168 |
--------------------------------------------------------------------------------
/example/windows/runner/win32_window.cpp:
--------------------------------------------------------------------------------
1 | #include "win32_window.h"
2 |
3 | #include
4 | #include
5 |
6 | #include "resource.h"
7 |
8 | namespace {
9 |
10 | /// Window attribute that enables dark mode window decorations.
11 | ///
12 | /// Redefined in case the developer's machine has a Windows SDK older than
13 | /// version 10.0.22000.0.
14 | /// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
15 | #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
16 | #define DWMWA_USE_IMMERSIVE_DARK_MODE 20
17 | #endif
18 |
19 | constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
20 |
21 | /// Registry key for app theme preference.
22 | ///
23 | /// A value of 0 indicates apps should use dark mode. A non-zero or missing
24 | /// value indicates apps should use light mode.
25 | constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
26 | L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
27 | constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
28 |
29 | // The number of Win32Window objects that currently exist.
30 | static int g_active_window_count = 0;
31 |
32 | using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
33 |
34 | // Scale helper to convert logical scaler values to physical using passed in
35 | // scale factor
36 | int Scale(int source, double scale_factor) {
37 | return static_cast(source * scale_factor);
38 | }
39 |
40 | // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
41 | // This API is only needed for PerMonitor V1 awareness mode.
42 | void EnableFullDpiSupportIfAvailable(HWND hwnd) {
43 | HMODULE user32_module = LoadLibraryA("User32.dll");
44 | if (!user32_module) {
45 | return;
46 | }
47 | auto enable_non_client_dpi_scaling =
48 | reinterpret_cast(
49 | GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
50 | if (enable_non_client_dpi_scaling != nullptr) {
51 | enable_non_client_dpi_scaling(hwnd);
52 | }
53 | FreeLibrary(user32_module);
54 | }
55 |
56 | } // namespace
57 |
58 | // Manages the Win32Window's window class registration.
59 | class WindowClassRegistrar {
60 | public:
61 | ~WindowClassRegistrar() = default;
62 |
63 | // Returns the singleton registrar instance.
64 | static WindowClassRegistrar* GetInstance() {
65 | if (!instance_) {
66 | instance_ = new WindowClassRegistrar();
67 | }
68 | return instance_;
69 | }
70 |
71 | // Returns the name of the window class, registering the class if it hasn't
72 | // previously been registered.
73 | const wchar_t* GetWindowClass();
74 |
75 | // Unregisters the window class. Should only be called if there are no
76 | // instances of the window.
77 | void UnregisterWindowClass();
78 |
79 | private:
80 | WindowClassRegistrar() = default;
81 |
82 | static WindowClassRegistrar* instance_;
83 |
84 | bool class_registered_ = false;
85 | };
86 |
87 | WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
88 |
89 | const wchar_t* WindowClassRegistrar::GetWindowClass() {
90 | if (!class_registered_) {
91 | WNDCLASS window_class{};
92 | window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
93 | window_class.lpszClassName = kWindowClassName;
94 | window_class.style = CS_HREDRAW | CS_VREDRAW;
95 | window_class.cbClsExtra = 0;
96 | window_class.cbWndExtra = 0;
97 | window_class.hInstance = GetModuleHandle(nullptr);
98 | window_class.hIcon =
99 | LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
100 | window_class.hbrBackground = 0;
101 | window_class.lpszMenuName = nullptr;
102 | window_class.lpfnWndProc = Win32Window::WndProc;
103 | RegisterClass(&window_class);
104 | class_registered_ = true;
105 | }
106 | return kWindowClassName;
107 | }
108 |
109 | void WindowClassRegistrar::UnregisterWindowClass() {
110 | UnregisterClass(kWindowClassName, nullptr);
111 | class_registered_ = false;
112 | }
113 |
114 | Win32Window::Win32Window() {
115 | ++g_active_window_count;
116 | }
117 |
118 | Win32Window::~Win32Window() {
119 | --g_active_window_count;
120 | Destroy();
121 | }
122 |
123 | bool Win32Window::Create(const std::wstring& title,
124 | const Point& origin,
125 | const Size& size) {
126 | Destroy();
127 |
128 | const wchar_t* window_class =
129 | WindowClassRegistrar::GetInstance()->GetWindowClass();
130 |
131 | const POINT target_point = {static_cast(origin.x),
132 | static_cast(origin.y)};
133 | HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
134 | UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
135 | double scale_factor = dpi / 96.0;
136 |
137 | HWND window = CreateWindow(
138 | window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
139 | Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
140 | Scale(size.width, scale_factor), Scale(size.height, scale_factor),
141 | nullptr, nullptr, GetModuleHandle(nullptr), this);
142 |
143 | if (!window) {
144 | return false;
145 | }
146 |
147 | UpdateTheme(window);
148 |
149 | return OnCreate();
150 | }
151 |
152 | bool Win32Window::Show() {
153 | return ShowWindow(window_handle_, SW_SHOWNORMAL);
154 | }
155 |
156 | // static
157 | LRESULT CALLBACK Win32Window::WndProc(HWND const window,
158 | UINT const message,
159 | WPARAM const wparam,
160 | LPARAM const lparam) noexcept {
161 | if (message == WM_NCCREATE) {
162 | auto window_struct = reinterpret_cast(lparam);
163 | SetWindowLongPtr(window, GWLP_USERDATA,
164 | reinterpret_cast(window_struct->lpCreateParams));
165 |
166 | auto that = static_cast(window_struct->lpCreateParams);
167 | EnableFullDpiSupportIfAvailable(window);
168 | that->window_handle_ = window;
169 | } else if (Win32Window* that = GetThisFromHandle(window)) {
170 | return that->MessageHandler(window, message, wparam, lparam);
171 | }
172 |
173 | return DefWindowProc(window, message, wparam, lparam);
174 | }
175 |
176 | LRESULT
177 | Win32Window::MessageHandler(HWND hwnd,
178 | UINT const message,
179 | WPARAM const wparam,
180 | LPARAM const lparam) noexcept {
181 | switch (message) {
182 | case WM_DESTROY:
183 | window_handle_ = nullptr;
184 | Destroy();
185 | if (quit_on_close_) {
186 | PostQuitMessage(0);
187 | }
188 | return 0;
189 |
190 | case WM_DPICHANGED: {
191 | auto newRectSize = reinterpret_cast(lparam);
192 | LONG newWidth = newRectSize->right - newRectSize->left;
193 | LONG newHeight = newRectSize->bottom - newRectSize->top;
194 |
195 | SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
196 | newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
197 |
198 | return 0;
199 | }
200 | case WM_SIZE: {
201 | RECT rect = GetClientArea();
202 | if (child_content_ != nullptr) {
203 | // Size and position the child window.
204 | MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
205 | rect.bottom - rect.top, TRUE);
206 | }
207 | return 0;
208 | }
209 |
210 | case WM_ACTIVATE:
211 | if (child_content_ != nullptr) {
212 | SetFocus(child_content_);
213 | }
214 | return 0;
215 |
216 | case WM_DWMCOLORIZATIONCOLORCHANGED:
217 | UpdateTheme(hwnd);
218 | return 0;
219 | }
220 |
221 | return DefWindowProc(window_handle_, message, wparam, lparam);
222 | }
223 |
224 | void Win32Window::Destroy() {
225 | OnDestroy();
226 |
227 | if (window_handle_) {
228 | DestroyWindow(window_handle_);
229 | window_handle_ = nullptr;
230 | }
231 | if (g_active_window_count == 0) {
232 | WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
233 | }
234 | }
235 |
236 | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
237 | return reinterpret_cast(
238 | GetWindowLongPtr(window, GWLP_USERDATA));
239 | }
240 |
241 | void Win32Window::SetChildContent(HWND content) {
242 | child_content_ = content;
243 | SetParent(content, window_handle_);
244 | RECT frame = GetClientArea();
245 |
246 | MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
247 | frame.bottom - frame.top, true);
248 |
249 | SetFocus(child_content_);
250 | }
251 |
252 | RECT Win32Window::GetClientArea() {
253 | RECT frame;
254 | GetClientRect(window_handle_, &frame);
255 | return frame;
256 | }
257 |
258 | HWND Win32Window::GetHandle() {
259 | return window_handle_;
260 | }
261 |
262 | void Win32Window::SetQuitOnClose(bool quit_on_close) {
263 | quit_on_close_ = quit_on_close;
264 | }
265 |
266 | bool Win32Window::OnCreate() {
267 | // No-op; provided for subclasses.
268 | return true;
269 | }
270 |
271 | void Win32Window::OnDestroy() {
272 | // No-op; provided for subclasses.
273 | }
274 |
275 | void Win32Window::UpdateTheme(HWND const window) {
276 | DWORD light_mode;
277 | DWORD light_mode_size = sizeof(light_mode);
278 | LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
279 | kGetPreferredBrightnessRegValue,
280 | RRF_RT_REG_DWORD, nullptr, &light_mode,
281 | &light_mode_size);
282 |
283 | if (result == ERROR_SUCCESS) {
284 | BOOL enable_dark_mode = light_mode == 0;
285 | DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
286 | &enable_dark_mode, sizeof(enable_dark_mode));
287 | }
288 | }
289 |
--------------------------------------------------------------------------------