├── .gitignore
├── .metadata
├── CHANGELOG.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── assets
├── lottie
│ └── speech_loading.json
└── svg
│ ├── chat
│ └── arrow_down.svg
│ └── input
│ ├── album.svg
│ ├── camera.svg
│ ├── send.svg
│ └── send_disabled.svg
├── build.yaml
├── devtools_options.yaml
├── example
├── .gitignore
├── .metadata
├── .vscode
│ └── launch.json
├── README.md
├── analysis_options.yaml
├── android
│ ├── .gitignore
│ ├── app
│ │ ├── build.gradle
│ │ └── src
│ │ │ ├── debug
│ │ │ └── AndroidManifest.xml
│ │ │ ├── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── kotlin
│ │ │ │ └── com
│ │ │ │ │ └── example
│ │ │ │ │ └── example
│ │ │ │ │ └── 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
├── devtools_options.yaml
├── 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
│ ├── custom_message.dart
│ ├── custom_message_view.dart
│ └── main.dart
├── macos
│ ├── .gitignore
│ ├── Flutter
│ │ ├── Flutter-Debug.xcconfig
│ │ ├── Flutter-Release.xcconfig
│ │ └── GeneratedPluginRegistrant.swift
│ ├── Podfile
│ ├── Podfile.lock
│ ├── 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
├── lib
├── controllers
│ ├── chat_action_handler.dart
│ ├── chat_controller.dart
│ ├── chat_scroll_controller.dart
│ └── view_factory.dart
├── models
│ ├── base_message.dart
│ ├── base_user.dart
│ ├── image_message.dart
│ ├── image_message.g.dart
│ ├── loading_indicator_message.dart
│ ├── message_send_output.dart
│ ├── text_message.dart
│ ├── text_message.g.dart
│ ├── user.dart
│ └── user.g.dart
├── simple_chat.dart
├── stores
│ ├── chat_store.dart
│ ├── chat_store.g.dart
│ ├── sequential_map.dart
│ └── sequential_map.g.dart
├── theme
│ └── chat_theme.dart
└── widgets
│ ├── chat_view.dart
│ ├── input
│ ├── input_box.dart
│ ├── input_box_image_item.dart
│ ├── input_box_text_field.dart
│ └── send_msg_btn.dart
│ ├── messages
│ ├── image_message_item.dart
│ ├── loading_indicator_item.dart
│ ├── message_bubble.dart
│ ├── text_message_item.dart
│ └── unsupport_message_item.dart
│ └── users
│ └── user_avatar.dart
├── pubspec.lock
└── pubspec.yaml
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | migrate_working_dir/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | **/ios/Flutter/.last_build_id
27 | .dart_tool/
28 | .flutter-plugins
29 | .flutter-plugins-dependencies
30 | .packages
31 | .pub-cache/
32 | .pub/
33 | /build/
34 |
35 | # Symbolication related
36 | app.*.symbols
37 |
38 | # Obfuscation related
39 | app.*.map.json
40 |
41 | # Android Studio will place build artifacts here
42 | /android/app/debug
43 | /android/app/profile
44 | /android/app/release
45 |
46 | # andr
47 | android/local.properties
48 |
49 | # 3rd part dependencies
50 | macos/fastlane/report.xml
51 | macos/fastlane/README.md
52 |
53 | # env variable
54 | .env
55 | **env.g.dart
56 | .vscode/settings.json
--------------------------------------------------------------------------------
/.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: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
8 | channel: "stable"
9 |
10 | project_type: package
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.0.1
2 |
3 | - initial release
4 |
5 | ## 0.0.2
6 |
7 | - add loading indicators
8 |
9 | ## 0.0.3
10 |
11 | - add message unread indicator
12 |
13 | ## 0.0.4
14 |
15 | - update sorting logic
16 |
17 | ## 0.0.5
18 |
19 | - add message status indicator
20 |
21 | ## 0.0.9
22 |
23 | - replace image picker
24 |
25 | ## 0.0.10
26 |
27 | - fix message avatar display
28 |
29 | - add config for photo permission denied ui
30 |
31 | - hide camera/album button when config image count is zero
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Tealseed
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple Chat
2 |
3 | A simple UI solution for quick integration of IM chat & AI bot chat.
4 |
5 | Supports customised Message Cell, message grouping, image preview and more.
6 |
7 | [](https://tealseed.com)
8 |
9 | ## Supported Platforms
10 |
11 | - iOS
12 | - Android
13 |
14 | ## Screenshots
15 | |
|
16 | |-------------------------|-------------------------|
17 |
18 | ## Basics
19 |
20 | ### Initialise Controller & View
21 |
22 | `ChatActionHandler` is used to handle events like sending message, user avatar tapping, image preview thumbntail tapping, etc.
23 | `ChatConfig` is optional, you can customise the input box hint text, image max count, etc.
24 |
25 | ```dart
26 | final controller = ChatController(
27 | config: ChatConfig(
28 | inputBoxHintText: 'Type a message...',
29 | ),
30 | actionHandler: ChatActionHandler(
31 | onSendMessage: (output) {},
32 | ),
33 | );
34 |
35 | ChatView(
36 | controller: controller,
37 | theme: ChatThemeData(
38 | dark: coloredThemeData,
39 | light: coloredThemeData,
40 | ),
41 | )
42 | ```
43 |
44 | ### Config Users
45 |
46 | Users configured with `id`, `name`, `avatarUrl` and `isCurrentUser` flag.
47 | - `id` is used to identify the user and later display messages under the corresponding user avatar.
48 | - `name` is used to display in the chat UI.
49 | - `avatarUrl` is used to display the user avatar.
50 | - `isCurrentUser` is used to identify if the user is the current user.
51 |
52 | ```dart
53 | await controller.store.addUsers(users: [
54 | ModelUser(
55 | id: '1',
56 | name: 'Lawrence',
57 | avatarUrl: 'https://example.com/avatar/1.png',
58 | isCurrentUser: true,
59 | ),
60 | ModelUser(
61 | id: '2',
62 | name: 'Ciel',
63 | avatarUrl: 'https://example.com/avatar/2.png',
64 | isCurrentUser: false,
65 | ),
66 | ]);
67 | ```
68 |
69 | ### Add messages
70 |
71 | - `isInitial` is used to identify if the message is historial message and determine if the message is shown with animation or not.
72 | - `userId` is used to identify the user who sent the message.
73 | - `sequence` is used to determine the order of the message.
74 | - `displayDatetime` is used to determine the display datetime of the message.
75 |
76 | ```dart
77 | await controller.store.addMessage(
78 | isInitial: !withDelay,
79 | message: ModelTextMessage(
80 | id: '$i',
81 | text: 'Hello, how are you?',
82 | userId: '1',
83 | sequence: i,
84 | displayDatetime: DateTime.now(),
85 | ),
86 | );
87 | ```
88 |
89 | ### Loading Indicator
90 | #### Blocking and show loading indicator
91 | |
|
92 | |-------------------------|
93 | ```dart
94 | final controller = ChatController(
95 | config: ChatConfig(
96 | loadingIndicatorType: LoadingIndicatorType.sendBtnLoading,
97 | ),
98 | );
99 | ```
100 |
101 | #### Non-blocking and show reply generator
102 | |
|
103 | |-------------------------|
104 | ```dart
105 | await controller.store.showReplyGeneratingIndicator();
106 | await Future.delayed(const Duration(seconds: 3));
107 | await controller.store.hideReplyGeneratingIndicator();
108 | ```
109 | ### Unread indicator
110 |
111 | #### Indicator with unread count
112 | |
|
113 | |-------------------------|
114 | ```dart
115 | final controller = ChatController(
116 | config: ChatConfig(
117 | showUnreadCount: true,
118 | ),
119 | ...
120 | );
121 | ```
122 | #### Indicator without unread count
123 | |
|
124 | |-------------------------|
125 | ```dart
126 | final controller = ChatController(
127 | config: ChatConfig(
128 | showUnreadCount: false,
129 | ),
130 | ...
131 | );
132 | ```
133 |
134 | ### Message status
135 | For each message we can also set status, like `sending`, `failed to send`.
136 | |
|
137 | |-------------------------|
138 | ```dart
139 | await controller.store.updateSendStatus(
140 | messageId: messageId,
141 | status: ModelBaseMessageStatus.sending,
142 | );
143 | await controller.store.updateSendStatus(
144 | messageId: messageId,
145 | status: ModelBaseMessageStatus.failedToSend,
146 | );
147 | ```
148 |
149 |
150 | ## Customisation
151 | ### Add your custom tool bar
152 | Add a tool bar sticked on the top of the input box and move together while keyboard shown/dismissed.
153 |
154 | |
|
155 | |-------------------------|
156 |
157 | ```dart
158 | ChatView(
159 | toolbar: Container(
160 | // custom toolbar
161 | ),
162 | ),
163 | ```
164 |
165 | ### Add your custom message cell UI
166 |
167 | #### Define your custom message model
168 |
169 | Normally if one user send multiple messages without interruption, they will be grouped together in one message cell.
170 | If you want to group them in different message cells, you can set `forceNewBlock` to `true`.
171 |
172 | ```dart
173 | class CustomMessage extends ModelBaseMessage {
174 | @override
175 | final String id;
176 |
177 | @override
178 | final String userId;
179 |
180 | @override
181 | final int sequence;
182 |
183 | @override
184 | final DateTime displayDatetime;
185 |
186 | @override
187 | final bool forceNewBlock;
188 |
189 | final String data;
190 |
191 | CustomMessage({
192 | required this.id,
193 | required this.userId,
194 | required this.sequence,
195 | required this.displayDatetime,
196 | required this.forceNewBlock,
197 | required this.data,
198 | });
199 | }
200 |
201 | ```
202 |
203 | #### Define your custom message cell UI
204 |
205 | You can use `MessageBubble` to wrap your custom message cell UI.
206 |
207 | ```dart
208 |
209 | class CustomMessageCell extends StatelessWidget {
210 | final ModelLoadingIndicatorMessage message;
211 | final bool isMessageFromCurrentUser;
212 | CustomMessageCell({
213 | super.key,
214 | required this.message,
215 | required this.isMessageFromCurrentUser,
216 | });
217 |
218 | @override
219 | Widget build(BuildContext context) {
220 | return MessageBubble(
221 | isCurrentUser: isMessageFromCurrentUser,
222 | padding: const EdgeInsets.all(12),
223 | ...
224 | );
225 | }
226 | }
227 |
228 | ```
229 |
230 | #### Register your custom message cell UI with custom message model
231 |
232 | After registeration, whenever a message with the custom message model is added, the registered UI will be used to display the message.
233 |
234 | ```dart
235 | controller.viewFactory.register(
236 | (BuildContext context, {
237 | required CustomMessage message,
238 | required bool isMessageFromCurrentUser,
239 | }) =>
240 | CustomMessageCell(
241 | message: message,
242 | isMessageFromCurrentUser: isMessageFromCurrentUser,
243 | ),
244 | );
245 | ```
246 | #### message cell with customised padding, show/hide avatar
247 |
248 | ```dart
249 | class CustomMessage extends ModelBaseMessage {
250 | ...
251 | @override
252 | // this is to indicate whether to show/hide avatar & avatar placehoding spacing
253 | bool get showAvatarAndPaddings => false;
254 |
255 | @override
256 | // this is to customise message view padding
257 | EdgeInsets? get customContainerPadding => EdgeInsets.zero;
258 | }
259 | ```
260 |
261 | The above example will give message view zero padding and hide user avatar for message cell rendering
262 |
263 | |
|
264 | |-------------------------|
265 |
266 | ## 🌟 Star History
267 |
268 | [](https://star-history.com/#Tealseed-Lab/simple_chat&Date)
269 |
270 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:flutter_lints/flutter.yaml
2 |
3 | # Additional information about this file can be found at
4 | # https://dart.dev/guides/language/analysis-options
5 |
6 | linter:
7 | rules:
8 | - public_member_api_docs
--------------------------------------------------------------------------------
/assets/lottie/speech_loading.json:
--------------------------------------------------------------------------------
1 | {"nm":"Flow 1","ddd":0,"h":66,"w":144,"meta":{"g":"LottieFiles Figma v67"},"layers":[{"ty":4,"nm":"Ellipse 1329130808","sr":1,"st":0,"op":91,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[12,12],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[12,12],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[18,18],"t":60},{"s":[12,12],"t":90}]},"s":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100,100],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100,100],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100,100],"t":60},{"s":[100,100],"t":90}]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[132,33],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[132,33],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[126,33],"t":60},{"s":[132,33],"t":90}]},"r":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0],"t":60},{"s":[0],"t":90}]},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":60},{"s":[100],"t":90}]}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[{"c":true,"i":[[0,0],[6.63,0],[0,6.63],[-6.63,0],[0,-6.63]],"o":[[0,6.63],[-6.63,0],[0,-6.63],[6.63,0],[0,0]],"v":[[24,12],[12,24],[0,12],[12,0],[24,12]]}],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[{"c":true,"i":[[0,0],[6.63,0],[0,6.63],[-6.63,0],[0,-6.63]],"o":[[0,6.63],[-6.63,0],[0,-6.63],[6.63,0],[0,0]],"v":[[24,12],[12,24],[0,12],[12,0],[24,12]]}],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[{"c":true,"i":[[0,0],[9.94,0],[0,9.94],[-9.94,0],[0,-9.94]],"o":[[0,9.94],[-9.94,0],[0,-9.94],[9.94,0],[0,0]],"v":[[36,18],[18,36],[0,18],[18,0],[36,18]]}],"t":60},{"s":[{"c":true,"i":[[0,0],[6.63,0],[0,6.63],[-6.63,0],[0,-6.63]],"o":[[0,6.63],[-6.63,0],[0,-6.63],[6.63,0],[0,0]],"v":[[24,12],[12,24],[0,12],[12,0],[24,12]]}],"t":90}]}},{"ty":"fl","bm":0,"hd":false,"nm":"","c":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0.8981,0.8981,0.9177],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0.8981,0.8981,0.9177],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0.8981,0.8981,0.9177],"t":60},{"s":[0.8981,0.8981,0.9177],"t":90}]},"r":1,"o":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":60},{"s":[100],"t":90}]}}],"ind":1},{"ty":4,"nm":"Ellipse 1329130807","sr":1,"st":0,"op":91,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[12,12],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[18,18],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[12,12],"t":60},{"s":[12,12],"t":90}]},"s":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100,100],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100,100],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100,100],"t":60},{"s":[100,100],"t":90}]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[78,33],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[75,33],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[72,33],"t":60},{"s":[78,33],"t":90}]},"r":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0],"t":60},{"s":[0],"t":90}]},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":60},{"s":[100],"t":90}]}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[{"c":true,"i":[[0,0],[6.63,0],[0,6.63],[-6.63,0],[0,-6.63]],"o":[[0,6.63],[-6.63,0],[0,-6.63],[6.63,0],[0,0]],"v":[[24,12],[12,24],[0,12],[12,0],[24,12]]}],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[{"c":true,"i":[[0,0],[9.94,0],[0,9.94],[-9.94,0],[0,-9.94]],"o":[[0,9.94],[-9.94,0],[0,-9.94],[9.94,0],[0,0]],"v":[[36,18],[18,36],[0,18],[18,0],[36,18]]}],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[{"c":true,"i":[[0,0],[6.63,0],[0,6.63],[-6.63,0],[0,-6.63]],"o":[[0,6.63],[-6.63,0],[0,-6.63],[6.63,0],[0,0]],"v":[[24,12],[12,24],[0,12],[12,0],[24,12]]}],"t":60},{"s":[{"c":true,"i":[[0,0],[6.63,0],[0,6.63],[-6.63,0],[0,-6.63]],"o":[[0,6.63],[-6.63,0],[0,-6.63],[6.63,0],[0,0]],"v":[[24,12],[12,24],[0,12],[12,0],[24,12]]}],"t":90}]}},{"ty":"fl","bm":0,"hd":false,"nm":"","c":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0.8981,0.8981,0.9177],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0.8981,0.8981,0.9177],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0.8981,0.8981,0.9177],"t":60},{"s":[0.8981,0.8981,0.9177],"t":90}]},"r":1,"o":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":60},{"s":[100],"t":90}]}}],"ind":2},{"ty":4,"nm":"Ellipse 1329130806","sr":1,"st":0,"op":91,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[18,18],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[12,12],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[12,12],"t":60},{"s":[18,18],"t":90}]},"s":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100,100],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100,100],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100,100],"t":60},{"s":[100,100],"t":90}]},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[18,33],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[12,33],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[12,33],"t":60},{"s":[18,33],"t":90}]},"r":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0],"t":60},{"s":[0],"t":90}]},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":60},{"s":[100],"t":90}]}},"shapes":[{"ty":"sh","bm":0,"hd":false,"nm":"","d":1,"ks":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[{"c":true,"i":[[0,0],[9.94,0],[0,9.94],[-9.94,0],[0,-9.94]],"o":[[0,9.94],[-9.94,0],[0,-9.94],[9.94,0],[0,0]],"v":[[36,18],[18,36],[0,18],[18,0],[36,18]]}],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[{"c":true,"i":[[0,0],[6.63,0],[0,6.63],[-6.63,0],[0,-6.63]],"o":[[0,6.63],[-6.63,0],[0,-6.63],[6.63,0],[0,0]],"v":[[24,12],[12,24],[0,12],[12,0],[24,12]]}],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[{"c":true,"i":[[0,0],[6.63,0],[0,6.63],[-6.63,0],[0,-6.63]],"o":[[0,6.63],[-6.63,0],[0,-6.63],[6.63,0],[0,0]],"v":[[24,12],[12,24],[0,12],[12,0],[24,12]]}],"t":60},{"s":[{"c":true,"i":[[0,0],[9.94,0],[0,9.94],[-9.94,0],[0,-9.94]],"o":[[0,9.94],[-9.94,0],[0,-9.94],[9.94,0],[0,0]],"v":[[36,18],[18,36],[0,18],[18,0],[36,18]]}],"t":90}]}},{"ty":"fl","bm":0,"hd":false,"nm":"","c":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0.8981,0.8981,0.9177],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0.8981,0.8981,0.9177],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[0.8981,0.8981,0.9177],"t":60},{"s":[0.8981,0.8981,0.9177],"t":90}]},"r":1,"o":{"a":1,"k":[{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":0},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":30},{"o":{"x":0.33,"y":1},"i":{"x":0.68,"y":1},"s":[100],"t":60},{"s":[100],"t":90}]}}],"ind":3}],"v":"5.7.0","fr":60,"op":90,"ip":0,"assets":[]}
--------------------------------------------------------------------------------
/assets/svg/chat/arrow_down.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/svg/input/album.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/svg/input/camera.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/assets/svg/input/send.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/svg/input/send_disabled.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/build.yaml:
--------------------------------------------------------------------------------
1 | targets:
2 | $default:
3 | sources:
4 | exclude:
5 | - example/**
--------------------------------------------------------------------------------
/devtools_options.yaml:
--------------------------------------------------------------------------------
1 | description: This file stores settings for Dart & Flutter DevTools.
2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
3 | extensions:
4 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .build/
9 | .buildlog/
10 | .history
11 | .svn/
12 | .swiftpm/
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 | .pub-cache/
33 | .pub/
34 | /build/
35 |
36 | # Symbolication related
37 | app.*.symbols
38 |
39 | # Obfuscation related
40 | app.*.map.json
41 |
42 | # Android Studio will place build artifacts here
43 | /android/app/debug
44 | /android/app/profile
45 | /android/app/release
46 |
--------------------------------------------------------------------------------
/example/.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: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
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: 761747bfc538b5af34aa0d3fac380f1bc331ec49
17 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
18 | - platform: android
19 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
20 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
21 | - platform: ios
22 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
23 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
24 | - platform: linux
25 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
26 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
27 | - platform: macos
28 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
29 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
30 | - platform: web
31 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
32 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
33 | - platform: windows
34 | create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
35 | base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
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 |
--------------------------------------------------------------------------------
/example/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "example",
9 | "request": "launch",
10 | "type": "dart"
11 | },
12 | {
13 | "name": "example (profile mode)",
14 | "request": "launch",
15 | "type": "dart",
16 | "flutterMode": "profile"
17 | },
18 | {
19 | "name": "example (release mode)",
20 | "request": "launch",
21 | "type": "dart",
22 | "flutterMode": "release"
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # example
2 |
3 | A new Flutter project.
4 |
5 | ## Getting Started
6 |
7 | This project is a starting point for a Flutter application.
8 |
9 | A few resources to get you started if this is your first Flutter project:
10 |
11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
13 |
14 | For help getting started with Flutter development, view the
15 | [online documentation](https://docs.flutter.dev/), which offers tutorials,
16 | samples, guidance on mobile development, and a full API reference.
17 |
--------------------------------------------------------------------------------
/example/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 https://dart.dev/lints.
17 | #
18 | # Instead of disabling a lint rule for the entire project in the
19 | # section below, it can also be suppressed for a single line of code
20 | # or a specific dart file by using the `// ignore: name_of_lint` and
21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
22 | # producing the lint.
23 | rules:
24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
26 |
27 | # Additional information about this file can be found at
28 | # https://dart.dev/guides/language/analysis-options
29 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.android.application"
3 | id "kotlin-android"
4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
5 | id "dev.flutter.flutter-gradle-plugin"
6 | }
7 |
8 | def localProperties = new Properties()
9 | def localPropertiesFile = rootProject.file("local.properties")
10 | if (localPropertiesFile.exists()) {
11 | localPropertiesFile.withReader("UTF-8") { reader ->
12 | localProperties.load(reader)
13 | }
14 | }
15 |
16 | def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
17 | if (flutterVersionCode == null) {
18 | flutterVersionCode = "1"
19 | }
20 |
21 | def flutterVersionName = localProperties.getProperty("flutter.versionName")
22 | if (flutterVersionName == null) {
23 | flutterVersionName = "1.0"
24 | }
25 |
26 | android {
27 | namespace = "com.example.example"
28 | compileSdk = flutter.compileSdkVersion
29 | ndkVersion = flutter.ndkVersion
30 |
31 | compileOptions {
32 | sourceCompatibility = JavaVersion.VERSION_1_8
33 | targetCompatibility = JavaVersion.VERSION_1_8
34 | }
35 |
36 | defaultConfig {
37 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
38 | applicationId = "com.example.example"
39 | // You can update the following values to match your application needs.
40 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
41 | minSdk = flutter.minSdkVersion
42 | targetSdk = flutter.targetSdkVersion
43 | versionCode = flutterVersionCode.toInteger()
44 | versionName = flutterVersionName
45 | }
46 |
47 | buildTypes {
48 | release {
49 | // TODO: Add your own signing config for the release build.
50 | // Signing with the debug keys for now, so `flutter run --release` works.
51 | signingConfig = signingConfigs.debug
52 | }
53 | }
54 | }
55 |
56 | flutter {
57 | source = "../.."
58 | }
59 |
--------------------------------------------------------------------------------
/example/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
15 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
30 |
33 |
34 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.example
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity()
6 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/example/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/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 |
10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
11 |
12 | repositories {
13 | google()
14 | mavenCentral()
15 | gradlePluginPortal()
16 | }
17 | }
18 |
19 | plugins {
20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0"
21 | id "com.android.application" version "7.3.0" apply false
22 | id "org.jetbrains.kotlin.android" version "1.7.10" apply false
23 | }
24 |
25 | include ":app"
26 |
--------------------------------------------------------------------------------
/example/devtools_options.yaml:
--------------------------------------------------------------------------------
1 | description: This file stores settings for Dart & Flutter DevTools.
2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
3 | extensions:
4 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/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 | 12.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '12.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 | end
44 | end
45 |
--------------------------------------------------------------------------------
/example/ios/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Flutter (1.0.0)
3 | - flutter_image_compress_common (1.0.0):
4 | - Flutter
5 | - Mantle
6 | - SDWebImage
7 | - SDWebImageWebPCoder
8 | - image_picker_ios (0.0.1):
9 | - Flutter
10 | - libwebp (1.3.2):
11 | - libwebp/demux (= 1.3.2)
12 | - libwebp/mux (= 1.3.2)
13 | - libwebp/sharpyuv (= 1.3.2)
14 | - libwebp/webp (= 1.3.2)
15 | - libwebp/demux (1.3.2):
16 | - libwebp/webp
17 | - libwebp/mux (1.3.2):
18 | - libwebp/demux
19 | - libwebp/sharpyuv (1.3.2)
20 | - libwebp/webp (1.3.2):
21 | - libwebp/sharpyuv
22 | - Mantle (2.2.0):
23 | - Mantle/extobjc (= 2.2.0)
24 | - Mantle/extobjc (2.2.0)
25 | - path_provider_foundation (0.0.1):
26 | - Flutter
27 | - FlutterMacOS
28 | - photo_manager (2.0.0):
29 | - Flutter
30 | - FlutterMacOS
31 | - SDWebImage (5.20.0):
32 | - SDWebImage/Core (= 5.20.0)
33 | - SDWebImage/Core (5.20.0)
34 | - SDWebImageWebPCoder (0.14.6):
35 | - libwebp (~> 1.0)
36 | - SDWebImage/Core (~> 5.17)
37 |
38 | DEPENDENCIES:
39 | - Flutter (from `Flutter`)
40 | - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
41 | - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
42 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
43 | - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
44 |
45 | SPEC REPOS:
46 | trunk:
47 | - libwebp
48 | - Mantle
49 | - SDWebImage
50 | - SDWebImageWebPCoder
51 |
52 | EXTERNAL SOURCES:
53 | Flutter:
54 | :path: Flutter
55 | flutter_image_compress_common:
56 | :path: ".symlinks/plugins/flutter_image_compress_common/ios"
57 | image_picker_ios:
58 | :path: ".symlinks/plugins/image_picker_ios/ios"
59 | path_provider_foundation:
60 | :path: ".symlinks/plugins/path_provider_foundation/darwin"
61 | photo_manager:
62 | :path: ".symlinks/plugins/photo_manager/ios"
63 |
64 | SPEC CHECKSUMS:
65 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
66 | flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1
67 | image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
68 | libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
69 | Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
70 | path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
71 | photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
72 | SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
73 | SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
74 |
75 | PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
76 |
77 | COCOAPODS: 1.16.2
78 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
49 |
50 |
51 |
52 |
53 |
64 |
66 |
72 |
73 |
74 |
75 |
81 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 |
4 | @main
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 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/example/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.
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleDisplayName
10 | Example
11 | CFBundleExecutable
12 | $(EXECUTABLE_NAME)
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | example
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 | Simple Chat needs to access your camera to take photo and send with message
31 | NSMicrophoneUsageDescription
32 | Simple Chat needs to access your microphone to take video and send with message
33 | NSPhotoLibraryUsageDescription
34 | Simple Chat needs to access your photo library to send message with photos
35 | UIApplicationSupportsIndirectInputEvents
36 |
37 | UILaunchStoryboardName
38 | LaunchScreen
39 | UIMainStoryboardFile
40 | Main
41 | UISupportedInterfaceOrientations
42 |
43 | UIInterfaceOrientationPortrait
44 | UIInterfaceOrientationLandscapeLeft
45 | UIInterfaceOrientationLandscapeRight
46 |
47 | UISupportedInterfaceOrientations~ipad
48 |
49 | UIInterfaceOrientationPortrait
50 | UIInterfaceOrientationPortraitUpsideDown
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/example/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/lib/custom_message.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:simple_chat/models/base_message.dart';
3 |
4 | class CustomMessage extends ModelBaseMessage {
5 | @override
6 | final String id;
7 |
8 | @override
9 | final String userId;
10 |
11 | @override
12 | final int sequence;
13 |
14 | @override
15 | final DateTime displayDatetime;
16 |
17 | @override
18 | final bool forceNewBlock;
19 |
20 | final String data;
21 |
22 | @override
23 | bool get showAvatarAndPaddings => false;
24 |
25 | @override
26 | EdgeInsets? get customContainerPadding => EdgeInsets.zero;
27 |
28 | CustomMessage({
29 | required this.id,
30 | required this.userId,
31 | required this.sequence,
32 | required this.displayDatetime,
33 | required this.forceNewBlock,
34 | required this.data,
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/example/lib/custom_message_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:example/custom_message.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | class CustomMessageView extends StatelessWidget {
5 | final CustomMessage message;
6 | final bool isMessageFromCurrentUser;
7 | const CustomMessageView({
8 | super.key,
9 | required this.message,
10 | required this.isMessageFromCurrentUser,
11 | });
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return Container(
16 | height: 100,
17 | width: MediaQuery.sizeOf(context).width,
18 | color: Colors.red,
19 | alignment: Alignment.center,
20 | child: const Text('this is a custom message'),
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/example/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math'; // Added import for Random
2 |
3 | import 'package:example/custom_message.dart';
4 | import 'package:example/custom_message_view.dart';
5 | import 'package:flutter/material.dart';
6 | import 'package:simple_chat/simple_chat.dart';
7 | import 'package:uuid/uuid.dart';
8 |
9 | void main() {
10 | runApp(const MyApp());
11 | }
12 |
13 | class MyApp extends StatelessWidget {
14 | const MyApp({super.key});
15 | @override
16 | Widget build(BuildContext context) {
17 | return const MaterialApp(
18 | home: HomePage(),
19 | );
20 | }
21 | }
22 |
23 | class HomePage extends StatefulWidget {
24 | const HomePage({
25 | super.key,
26 | });
27 | @override
28 | State createState() {
29 | return _HomePageState();
30 | }
31 | }
32 |
33 | class _HomePageState extends State {
34 | late final ChatController controller;
35 | var sequence = 0;
36 | final userId1 = const Uuid().v4();
37 | final userId2 = const Uuid().v4();
38 | @override
39 | void initState() {
40 | super.initState();
41 | controller = ChatController(
42 | config: ChatConfig(
43 | loadingIndicatorType: LoadingIndicatorType.noBlocking,
44 | showUnreadCount: true,
45 | imageMaxCount: 1,
46 | ),
47 | actionHandler: ChatActionHandler(
48 | onSendMessage: _handleSendingMessage,
49 | // onSendMessage: _handleSendingMessageWithStatus,
50 | ),
51 | );
52 | controller.viewFactory.register(
53 | (
54 | BuildContext context, {
55 | required CustomMessage message,
56 | required bool isMessageFromCurrentUser,
57 | }) =>
58 | CustomMessageView(
59 | message: message,
60 | isMessageFromCurrentUser: isMessageFromCurrentUser,
61 | ),
62 | );
63 | setupTests();
64 | }
65 |
66 | @override
67 | void dispose() {
68 | super.dispose();
69 | }
70 |
71 | Future _handleSendingMessage(ChatMessageSendOutput output) async {
72 | if (output.message.isNotEmpty) {
73 | await controller.store.addMessage(
74 | message: ModelTextMessage(
75 | id: const Uuid().v4(),
76 | text: output.message,
77 | userId: userId1,
78 | sequence: sequence++,
79 | displayDatetime: DateTime.now(),
80 | ),
81 | );
82 | }
83 | if (output.imageFiles.isNotEmpty) {
84 | // in reality, it's usually better to upload images to server first
85 | await controller.store.addMessage(
86 | message: ModelImageMessage(
87 | id: const Uuid().v4(),
88 | userId: userId1,
89 | sequence: sequence++,
90 | displayDatetime: DateTime.now(),
91 | imageUrls: output.imageFiles.map((e) => e.path).toList(),
92 | ),
93 | );
94 | }
95 | await controller.store.showReplyGeneratingIndicator();
96 | await Future.delayed(const Duration(seconds: 3));
97 | await controller.store.hideReplyGeneratingIndicator();
98 | await controller.store.addMessage(
99 | message: ModelTextMessage(
100 | id: const Uuid().v4(),
101 | userId: userId2,
102 | sequence: sequence++,
103 | displayDatetime: DateTime.now(),
104 | text: 'Example reply~',
105 | ),
106 | );
107 | }
108 |
109 | // ignore: unused_element
110 | Future _handleSendingMessageWithStatus(
111 | ChatMessageSendOutput output) async {
112 | if (output.message.isNotEmpty) {
113 | final messageId = const Uuid().v4();
114 | await controller.store.addMessage(
115 | message: ModelTextMessage(
116 | id: messageId,
117 | text: output.message,
118 | userId: userId1,
119 | sequence: sequence++,
120 | displayDatetime: DateTime.now(),
121 | ),
122 | );
123 | await Future.delayed(const Duration(seconds: 1));
124 | await controller.store.updateSendStatus(
125 | messageId: messageId,
126 | status: ModelBaseMessageStatus.sending,
127 | );
128 | await Future.delayed(const Duration(seconds: 1));
129 | await controller.store.updateSendStatus(
130 | messageId: messageId,
131 | status: ModelBaseMessageStatus.failedToSend,
132 | );
133 | }
134 | }
135 |
136 | Future setupTests() async {
137 | await injectUsers();
138 | }
139 |
140 | Future injectUsers() async {
141 | await controller.store.addUsers(users: [
142 | ModelUser(
143 | id: userId1,
144 | name: 'Lawrence',
145 | avatarUrl:
146 | 'https://lh3.googleusercontent.com/ogw/AF2bZyj1OQs6QwRQMGfY0H5g_VOdijzbC7Ea3XE3Z8eDYTrOZQ=s64-c-mo',
147 | isCurrentUser: true,
148 | ),
149 | ModelUser(
150 | id: userId2,
151 | name: 'Ciel',
152 | avatarUrl:
153 | 'https://media.karousell.com/media/photos/profiles/2018/01/09/imwithye_1515485479.jpg',
154 | isCurrentUser: false,
155 | ),
156 | ]);
157 | }
158 |
159 | Future injectMessages({required bool withDelay}) async {
160 | final random = Random();
161 | final totalCount = withDelay ? 1 : 100;
162 | for (var i = 0; i < totalCount; i++) {
163 | if (withDelay) {
164 | await Future.delayed(const Duration(milliseconds: 1000));
165 | }
166 | final userId = random.nextBool() ? userId1 : userId2;
167 | final textLength =
168 | random.nextInt(50) + 10; // Random length between 10 and 59
169 | await controller.store.addMessage(
170 | isInitial: !withDelay,
171 | message: ModelTextMessage(
172 | id: const Uuid().v4(),
173 | text: generateRandomText(textLength),
174 | userId: userId,
175 | sequence: sequence++,
176 | displayDatetime: DateTime.now(),
177 | ),
178 | );
179 | }
180 | }
181 |
182 | Future injectCustomMessage() async {
183 | await controller.store.addMessage(
184 | message: CustomMessage(
185 | id: const Uuid().v4(),
186 | userId: userId1,
187 | sequence: sequence++,
188 | displayDatetime: DateTime.now(),
189 | forceNewBlock: true,
190 | data: 'this is a custom message',
191 | ),
192 | );
193 | }
194 |
195 | String generateRandomText(int length) {
196 | const chars = 'abcdefghijklmnopqrstuvwxyz0123456789 ';
197 | final random = Random();
198 | return String.fromCharCodes(Iterable.generate(
199 | length, (_) => chars.codeUnitAt(random.nextInt(chars.length))));
200 | }
201 |
202 | @override
203 | Widget build(BuildContext context) {
204 | final coloredThemeData = ChatColorThemeData();
205 | return Scaffold(
206 | appBar: AppBar(
207 | title: const Text('Home Page'),
208 | actions: [
209 | PopupMenuButton(
210 | icon: const Icon(Icons.more_vert),
211 | onSelected: (value) {
212 | switch (value) {
213 | case 'scroll_top':
214 | controller.chatScrollController.scrollToTop();
215 | break;
216 | case 'scroll_bottom':
217 | controller.chatScrollController.scrollToBottom();
218 | break;
219 | case 'inject_messages':
220 | injectMessages(withDelay: false);
221 | break;
222 | case 'inject_messages_delay':
223 | injectMessages(withDelay: true);
224 | break;
225 | case 'clear_all':
226 | controller.store.clearAll();
227 | break;
228 | case 'inject_custom_message':
229 | injectCustomMessage();
230 | break;
231 | }
232 | },
233 | itemBuilder: (BuildContext context) => >[
234 | const PopupMenuItem(
235 | value: 'scroll_top',
236 | child: ListTile(
237 | leading: Icon(Icons.arrow_upward),
238 | title: Text('Scroll to Top'),
239 | ),
240 | ),
241 | const PopupMenuItem(
242 | value: 'scroll_bottom',
243 | child: ListTile(
244 | leading: Icon(Icons.arrow_downward),
245 | title: Text('Scroll to Bottom'),
246 | ),
247 | ),
248 | const PopupMenuItem(
249 | value: 'inject_messages',
250 | child: ListTile(
251 | leading: Icon(Icons.message),
252 | title: Text('Inject Messages'),
253 | ),
254 | ),
255 | const PopupMenuItem(
256 | value: 'inject_messages_delay',
257 | child: ListTile(
258 | leading: Icon(Icons.auto_awesome),
259 | title: Text('Inject Messages with Delay'),
260 | ),
261 | ),
262 | const PopupMenuItem(
263 | value: 'clear_all',
264 | child: ListTile(
265 | leading: Icon(Icons.clear),
266 | title: Text('Clear All'),
267 | ),
268 | ),
269 | const PopupMenuItem(
270 | value: 'inject_custom_message',
271 | child: ListTile(
272 | leading: Icon(Icons.message),
273 | title: Text('Inject Custom Message'),
274 | ),
275 | ),
276 | ],
277 | ),
278 | ],
279 | ),
280 | body: ChatView(
281 | controller: controller,
282 | theme: ChatThemeData(
283 | dark: coloredThemeData,
284 | light: coloredThemeData,
285 | ),
286 | toolbar: Container(
287 | color: Colors.white,
288 | height: 50,
289 | padding: const EdgeInsets.symmetric(
290 | horizontal: 16,
291 | vertical: 8,
292 | ),
293 | child: Row(
294 | crossAxisAlignment: CrossAxisAlignment.center,
295 | children: [
296 | CompositedTransformTarget(
297 | link: layerLink,
298 | child: ElevatedButton(
299 | onPressed: () {
300 | _showOverlay(context);
301 | },
302 | style: ElevatedButton.styleFrom(
303 | shape: RoundedRectangleBorder(
304 | borderRadius:
305 | BorderRadius.circular(12), // Rounded corners
306 | ),
307 | ),
308 | child: const Text("Filter"),
309 | ),
310 | ),
311 | ],
312 | ),
313 | ),
314 | ),
315 | );
316 | }
317 |
318 | final layerLink = LayerLink();
319 | OverlayEntry? overlayEntry;
320 |
321 | void _showOverlay(BuildContext context) {
322 | if (overlayEntry == null) {
323 | const overlayHeight = 100.0;
324 | const overlayWidth = 120.0;
325 | final overlay = Overlay.of(context);
326 |
327 | overlayEntry = OverlayEntry(builder: (context) {
328 | return Positioned(
329 | left: 0,
330 | top: 0,
331 | child: CompositedTransformFollower(
332 | showWhenUnlinked: false,
333 | link: layerLink,
334 | offset: const Offset(0, -overlayHeight - 4),
335 | child: Material(
336 | color: Colors.transparent,
337 | child: SizedBox(
338 | width: overlayWidth,
339 | height: overlayHeight,
340 | child: TapRegion(
341 | consumeOutsideTaps: true,
342 | onTapOutside: (event) => _hideOverlay(),
343 | child: Container(
344 | height: overlayHeight,
345 | width: overlayWidth,
346 | decoration: BoxDecoration(
347 | borderRadius: BorderRadius.circular(12),
348 | color: Colors.red,
349 | ),
350 | ),
351 | ),
352 | ),
353 | ),
354 | ),
355 | );
356 | });
357 | overlay.insert(overlayEntry!);
358 | } else {
359 | _hideOverlay();
360 | }
361 | }
362 |
363 | void _hideOverlay() {
364 | overlayEntry?.remove();
365 | overlayEntry = null;
366 | }
367 | }
368 |
--------------------------------------------------------------------------------
/example/macos/.gitignore:
--------------------------------------------------------------------------------
1 | # Flutter-related
2 | **/Flutter/ephemeral/
3 | **/Pods/
4 |
5 | # Xcode-related
6 | **/dgph
7 | **/xcuserdata/
8 |
--------------------------------------------------------------------------------
/example/macos/Flutter/Flutter-Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "ephemeral/Flutter-Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/macos/Flutter/Flutter-Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "ephemeral/Flutter-Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/macos/Flutter/GeneratedPluginRegistrant.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Generated file. Do not edit.
3 | //
4 |
5 | import FlutterMacOS
6 | import Foundation
7 |
8 | import file_selector_macos
9 | import flutter_image_compress_macos
10 | import path_provider_foundation
11 | import photo_manager
12 |
13 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
14 | FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
15 | FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
16 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
17 | PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "PhotoManagerPlugin"))
18 | }
19 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/macos/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - file_selector_macos (0.0.1):
3 | - FlutterMacOS
4 | - FlutterMacOS (1.0.0)
5 |
6 | DEPENDENCIES:
7 | - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
8 | - FlutterMacOS (from `Flutter/ephemeral`)
9 |
10 | EXTERNAL SOURCES:
11 | file_selector_macos:
12 | :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
13 | FlutterMacOS:
14 | :path: Flutter/ephemeral
15 |
16 | SPEC CHECKSUMS:
17 | file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
18 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
19 |
20 | PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
21 |
22 | COCOAPODS: 1.15.2
23 |
--------------------------------------------------------------------------------
/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/example/macos/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/macos/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import FlutterMacOS
3 |
4 | @main
5 | class AppDelegate: FlutterAppDelegate {
6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
7 | return true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
--------------------------------------------------------------------------------
/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
--------------------------------------------------------------------------------
/example/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 = example
9 |
10 | // The application's bundle identifier
11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example
12 |
13 | // The copyright displayed in application information
14 | PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved.
15 |
--------------------------------------------------------------------------------
/example/macos/Runner/Configs/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "../../Flutter/Flutter-Debug.xcconfig"
2 | #include "Warnings.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/macos/Runner/Configs/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "../../Flutter/Flutter-Release.xcconfig"
2 | #include "Warnings.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/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 |
--------------------------------------------------------------------------------
/example/macos/Runner/Release.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/macos/RunnerTests/RunnerTests.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import FlutterMacOS
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 |
--------------------------------------------------------------------------------
/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 | flutter:
32 | sdk: flutter
33 |
34 |
35 | # The following adds the Cupertino Icons font to your application.
36 | # Use with the CupertinoIcons class for iOS style icons.
37 | cupertino_icons: ^1.0.6
38 |
39 | uuid: ^4.5.0
40 | simple_chat:
41 | path: ../
42 |
43 | dev_dependencies:
44 | flutter_test:
45 | sdk: flutter
46 |
47 | # The "flutter_lints" package below contains a set of recommended lints to
48 | # encourage good coding practices. The lint set provided by the package is
49 | # activated in the `analysis_options.yaml` file located at the root of your
50 | # package. See that file for information about deactivating specific lint
51 | # rules and activating additional ones.
52 | flutter_lints: ^3.0.0
53 |
54 | # For information on the generic Dart part of this file, see the
55 | # following page: https://dart.dev/tools/pub/pubspec
56 |
57 | # The following section is specific to Flutter packages.
58 | flutter:
59 |
60 | # The following line ensures that the Material Icons font is
61 | # included with your application, so that you can use the icons in
62 | # the material Icons class.
63 | uses-material-design: true
64 |
65 | # To add assets to your application, add an assets section, like this:
66 | # assets:
67 | # - images/a_dot_burr.jpeg
68 | # - images/a_dot_ham.jpeg
69 |
70 | # An image asset can refer to one or more resolution-specific "variants", see
71 | # https://flutter.dev/assets-and-images/#resolution-aware
72 |
73 | # For details regarding adding assets from package dependencies, see
74 | # https://flutter.dev/assets-and-images/#from-packages
75 |
76 | # To add custom fonts to your application, add a fonts section here,
77 | # in this "flutter" section. Each entry in this list should have a
78 | # "family" key with the font family name, and a "fonts" key with a
79 | # list giving the asset and other descriptors for the font. For
80 | # example:
81 | # fonts:
82 | # - family: Schyler
83 | # fonts:
84 | # - asset: fonts/Schyler-Regular.ttf
85 | # - asset: fonts/Schyler-Italic.ttf
86 | # style: italic
87 | # - family: Trajan Pro
88 | # fonts:
89 | # - asset: fonts/TrajanPro.ttf
90 | # - asset: fonts/TrajanPro_Bold.ttf
91 | # weight: 700
92 | #
93 | # For details regarding fonts from package dependencies,
94 | # see https://flutter.dev/custom-fonts/#from-packages
95 |
--------------------------------------------------------------------------------
/example/web/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/web/favicon.png
--------------------------------------------------------------------------------
/example/web/icons/Icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/web/icons/Icon-192.png
--------------------------------------------------------------------------------
/example/web/icons/Icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/web/icons/Icon-512.png
--------------------------------------------------------------------------------
/example/web/icons/Icon-maskable-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/web/icons/Icon-maskable-192.png
--------------------------------------------------------------------------------
/example/web/icons/Icon-maskable-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tealseed-Lab/simple_chat/c75405a9b3cb5779e24368ccff1826c469d1ddce/example/web/icons/Icon-maskable-512.png
--------------------------------------------------------------------------------
/example/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | example
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/example/web/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "short_name": "example",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "background_color": "#0175C2",
7 | "theme_color": "#0175C2",
8 | "description": "A new Flutter project.",
9 | "orientation": "portrait-primary",
10 | "prefer_related_applications": false,
11 | "icons": [
12 | {
13 | "src": "icons/Icon-192.png",
14 | "sizes": "192x192",
15 | "type": "image/png"
16 | },
17 | {
18 | "src": "icons/Icon-512.png",
19 | "sizes": "512x512",
20 | "type": "image/png"
21 | },
22 | {
23 | "src": "icons/Icon-maskable-192.png",
24 | "sizes": "192x192",
25 | "type": "image/png",
26 | "purpose": "maskable"
27 | },
28 | {
29 | "src": "icons/Icon-maskable-512.png",
30 | "sizes": "512x512",
31 | "type": "image/png",
32 | "purpose": "maskable"
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/lib/controllers/chat_action_handler.dart:
--------------------------------------------------------------------------------
1 | import 'package:easy_asset_picker/picker/models/asset_image.dart';
2 | import 'package:simple_chat/models/base_message.dart';
3 | import 'package:simple_chat/models/base_user.dart';
4 | import 'package:simple_chat/models/message_send_output.dart';
5 |
6 | /// The handler for the chat actions.
7 | class ChatActionHandler {
8 | /// The callback for sending a message.
9 | final Future Function(ChatMessageSendOutput)? onSendMessage;
10 |
11 | /// The callback for tapping a message.
12 | final Future Function(ModelBaseMessage)? onMessageTap;
13 |
14 | /// The callback for tapping an image thumbnail.
15 | final Future Function(AssetImageInfo)? onImageThumbnailTap;
16 |
17 | /// The callback for tapping a user avatar.
18 | final Future Function(ModelBaseUser?)? onUserAvatarTap;
19 |
20 | /// The callback for tapping a message failed status.
21 | final Future Function(ModelBaseMessage)? onMessageFailedStatusTap;
22 |
23 | /// The constructor of [ChatActionHandler].
24 | ChatActionHandler({
25 | this.onSendMessage,
26 | this.onMessageTap,
27 | this.onImageThumbnailTap,
28 | this.onUserAvatarTap,
29 | this.onMessageFailedStatusTap,
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/lib/controllers/chat_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:simple_chat/controllers/chat_scroll_controller.dart';
3 | import 'package:simple_chat/controllers/view_factory.dart';
4 | import 'package:simple_chat/models/loading_indicator_message.dart';
5 | import 'package:simple_chat/simple_chat.dart';
6 | import 'package:simple_chat/widgets/messages/image_message_item.dart';
7 | import 'package:simple_chat/widgets/messages/loading_indicator_item.dart';
8 | import 'package:simple_chat/widgets/messages/text_message_item.dart';
9 |
10 | /// The type of the loading indicator.
11 | enum LoadingIndicatorType {
12 | /// The loading indicator type for the send button.
13 | sendBtnLoading,
14 |
15 | /// The loading indicator type for the no blocking.
16 | noBlocking,
17 | }
18 |
19 | /// The alignment of the message.
20 | enum MessageAlignment {
21 | /// The alignment of the message for the center.
22 | center,
23 |
24 | /// The alignment of the message for the justified.
25 | justified,
26 | }
27 |
28 | /// The config for the chat.
29 | class ChatConfig {
30 | /// The max count of the image.
31 | final int imageMaxCount;
32 |
33 | /// The hint text for the input box.
34 | final String? inputBoxHintText;
35 |
36 | /// The text for the failed to send.
37 | final String? failedToSendText;
38 |
39 | /// The text for the photo permission denied.
40 | final String? photoPermissionDeniedText;
41 |
42 | /// The text for the photo permission denied button.
43 | final String? photoPermissionDeniedButtonText;
44 |
45 | /// The type of the loading indicator.
46 | final LoadingIndicatorType loadingIndicatorType;
47 |
48 | /// The flag for the show unread count.
49 | final bool showUnreadCount;
50 |
51 | /// The alignment of the message.
52 | final MessageAlignment messageAlignment;
53 |
54 | /// The constructor of the chat config.
55 | ChatConfig({
56 | this.imageMaxCount = 9,
57 | this.inputBoxHintText,
58 | this.failedToSendText,
59 | this.photoPermissionDeniedText,
60 | this.photoPermissionDeniedButtonText,
61 | this.loadingIndicatorType = LoadingIndicatorType.sendBtnLoading,
62 | this.showUnreadCount = false,
63 | this.messageAlignment = MessageAlignment.center,
64 | });
65 | }
66 |
67 | /// The controller for the chat.
68 | class ChatController {
69 | /// The store for the chat.
70 | late final ChatStore store;
71 |
72 | /// The scroll controller for the chat.
73 | final chatScrollController = ChatScrollController();
74 |
75 | /// The view factory for the chat.
76 | final viewFactory = ViewFactory();
77 |
78 | /// The action handler for the chat.
79 | final ChatActionHandler? actionHandler;
80 |
81 | /// The config for the chat.
82 | late final ChatConfig config;
83 |
84 | /// The constructor of the chat controller.
85 | ChatController({
86 | this.actionHandler,
87 | ChatConfig? config,
88 | }) {
89 | this.config = config ?? ChatConfig();
90 | store = ChatStore(
91 | chatScrollController,
92 | this.config,
93 | );
94 | viewFactory.register(
95 | (
96 | BuildContext context, {
97 | required bool isMessageFromCurrentUser,
98 | required ModelTextMessage message,
99 | }) =>
100 | TextMessageItem(
101 | isMessageFromCurrentUser: isMessageFromCurrentUser,
102 | textMessage: message,
103 | ),
104 | );
105 | viewFactory.register(
106 | (
107 | BuildContext context, {
108 | required bool isMessageFromCurrentUser,
109 | required ModelImageMessage message,
110 | }) =>
111 | ImageMessageItem(
112 | isMessageFromCurrentUser: isMessageFromCurrentUser,
113 | imageMessage: message,
114 | ),
115 | );
116 | viewFactory.register(
117 | (
118 | BuildContext context, {
119 | required bool isMessageFromCurrentUser,
120 | required ModelLoadingIndicatorMessage message,
121 | }) =>
122 | LoadingIndicatorItem(
123 | message: message,
124 | isMessageFromCurrentUser: isMessageFromCurrentUser,
125 | ),
126 | );
127 | }
128 |
129 | /// The method for the scroll to bottom.
130 | void scrollToBottom() {
131 | chatScrollController.scrollToBottom();
132 | store.readAllMessages();
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/lib/controllers/chat_scroll_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:logger/logger.dart';
3 |
4 | /// The scroll controller for the chat.
5 | class ChatScrollController with WidgetsBindingObserver {
6 | /// The scroll controller for the chat.
7 | final controller = ScrollController();
8 |
9 | /// The animation duration in milliseconds.
10 | final int animationDurationInMilliseconds;
11 |
12 | /// The constructor of the chat scroll controller.
13 | ChatScrollController({
14 | this.animationDurationInMilliseconds = 150,
15 | }) {
16 | WidgetsBinding.instance.addObserver(this);
17 | }
18 |
19 | /// The method for the dispose.
20 | void dispose() {
21 | WidgetsBinding.instance.removeObserver(this);
22 | }
23 |
24 | // WidgetsBindingObserver
25 |
26 | double _previousBottomInset = 0;
27 |
28 | @override
29 | void didChangeMetrics() {
30 | super.didChangeMetrics();
31 | final bottomInset = WidgetsBinding
32 | .instance.platformDispatcher.views.first.viewInsets.bottom;
33 | // Only jump to bottom when keyboard is shown (bottomInset increases)
34 | if (bottomInset > _previousBottomInset) {
35 | jumpToBottom();
36 | }
37 | _previousBottomInset = bottomInset;
38 | }
39 |
40 | // public
41 |
42 | /// The method for the is at top.
43 | bool isAtTop() {
44 | if (!controller.hasClients) return false;
45 | final position = controller.position;
46 | return position.pixels <= 1;
47 | }
48 |
49 | /// The method for the is at bottom.
50 | bool isAtBottom() {
51 | if (!controller.hasClients) return false;
52 | final position = controller.position;
53 | return position.pixels >= position.maxScrollExtent - 1 ||
54 | position.viewportDimension >= position.maxScrollExtent;
55 | }
56 |
57 | /// The method for the scroll to top.
58 | Future scrollToTop() async {
59 | if (controller.hasClients) {
60 | if (controller.offset > 0) {
61 | await controller.animateTo(
62 | 0,
63 | duration: Duration(milliseconds: animationDurationInMilliseconds),
64 | curve: Curves.easeOut,
65 | );
66 | }
67 | }
68 | }
69 |
70 | /// The method for the scroll to bottom.
71 | Future scrollToBottom() async {
72 | if (controller.hasClients) {
73 | Logger().i(
74 | '[easy-chat-scroll-controller] scrollToBottom check offset: ${controller.offset}, maxScrollExtent: ${controller.position.maxScrollExtent}');
75 | while (controller.offset + 1 < controller.position.maxScrollExtent) {
76 | Logger().i(
77 | '[easy-chat-scroll-controller] scrollToBottom offset: ${controller.offset}, maxScrollExtent: ${controller.position.maxScrollExtent}');
78 | controller.animateTo(
79 | controller.position.maxScrollExtent,
80 | duration: Duration(milliseconds: animationDurationInMilliseconds),
81 | curve: Curves.easeOut,
82 | );
83 | await Future.delayed(
84 | Duration(milliseconds: animationDurationInMilliseconds),
85 | );
86 | }
87 | }
88 | }
89 |
90 | /// The method for the jump to top.
91 | Future jumpToTop() async {
92 | if (controller.hasClients) {
93 | if (controller.offset > 0) {
94 | controller.jumpTo(0);
95 | }
96 | }
97 | }
98 |
99 | /// The method for the jump to bottom.
100 | Future jumpToBottom() async {
101 | if (controller.hasClients) {
102 | while (controller.offset + 1 < controller.position.maxScrollExtent) {
103 | Logger().i(
104 | '[easy-chat-scroll-controller] jumpToBottom offset: ${controller.offset}, maxScrollExtent: ${controller.position.maxScrollExtent}');
105 | controller.jumpTo(
106 | controller.position.maxScrollExtent,
107 | );
108 | await Future.delayed(
109 | Duration(milliseconds: animationDurationInMilliseconds),
110 | );
111 | }
112 | }
113 | }
114 |
115 | /// The method for the get offset.
116 | double getOffset() {
117 | return controller.offset;
118 | }
119 |
120 | /// The method for the set offset.
121 | void setOffset(double offset) {
122 | controller.jumpTo(offset);
123 | }
124 |
125 | // private
126 | }
127 |
--------------------------------------------------------------------------------
/lib/controllers/view_factory.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:simple_chat/models/base_message.dart';
3 |
4 | /// The type of the message view builder.
5 | typedef MessageViewBuilder = Widget Function(
6 | BuildContext context, {
7 | required bool isMessageFromCurrentUser,
8 | required T message,
9 | });
10 |
11 | /// The view factory for the chat.
12 | class ViewFactory {
13 | /// The registry for the message view builder.
14 | final Map _registry = {};
15 |
16 | /// The method for the register.
17 | void register(MessageViewBuilder builder) {
18 | _registry[T] = (
19 | BuildContext context, {
20 | required bool isMessageFromCurrentUser,
21 | required ModelBaseMessage message,
22 | }) {
23 | if (message is T) {
24 | return builder(
25 | context,
26 | isMessageFromCurrentUser: isMessageFromCurrentUser,
27 | message: message,
28 | );
29 | } else {
30 | throw ArgumentError(
31 | 'Message type mismatch: expected ${T.toString()}, but got ${message.runtimeType}');
32 | }
33 | };
34 | }
35 |
36 | /// The method for the build.
37 | Widget? buildFor(
38 | BuildContext context, {
39 | required ModelBaseMessage message,
40 | required bool isMessageFromCurrentUser,
41 | }) {
42 | return _registry[message.runtimeType]?.call(
43 | context,
44 | isMessageFromCurrentUser: isMessageFromCurrentUser,
45 | message: message,
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/models/base_message.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | /// The status of the message.
4 | enum ModelBaseMessageStatus {
5 | /// developed
6 | /// The status of the message for the normal.
7 | normal,
8 |
9 | /// The status of the message for the failed to send.
10 | failedToSend,
11 |
12 | /// wip
13 | /// The status of the message for the sending.
14 | sending,
15 |
16 | /// The status of the message for the sent.
17 | sent,
18 |
19 | /// The status of the message for the received.
20 | received,
21 |
22 | /// The status of the message for the read.
23 | read,
24 | }
25 |
26 | /// The base message for the chat.
27 | abstract class ModelBaseMessage {
28 | /// The widget key for the message.
29 | final widgetKey = GlobalKey();
30 |
31 | /// The id of the message.
32 | String get id;
33 |
34 | /// The user id of the message.
35 | String get userId;
36 |
37 | /// The sequence of the message.
38 | int get sequence;
39 |
40 | /// The display datetime of the message.
41 | DateTime get displayDatetime;
42 |
43 | /// The force new block of the message.
44 | bool get forceNewBlock => false;
45 |
46 | /// The status of the message.
47 | ModelBaseMessageStatus status = ModelBaseMessageStatus.normal;
48 |
49 | /// to indicate whether to show avatar & paddings
50 | bool get showAvatarAndPaddings => true;
51 |
52 | /// custom padding for the message
53 | EdgeInsets? get customContainerPadding => null;
54 | }
55 |
--------------------------------------------------------------------------------
/lib/models/base_user.dart:
--------------------------------------------------------------------------------
1 | /// The base user for the chat.
2 | abstract class ModelBaseUser {
3 | /// The id of the user.
4 | String get id;
5 |
6 | /// The name of the user.
7 | String get name;
8 |
9 | /// The avatar url of the user.
10 | String? get avatarUrl;
11 |
12 | /// The flag for the current user.
13 | bool get isCurrentUser;
14 | }
15 |
--------------------------------------------------------------------------------
/lib/models/image_message.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 | import 'package:simple_chat/models/base_message.dart';
3 |
4 | part 'image_message.g.dart';
5 |
6 | /// The image message for the chat.
7 | @JsonSerializable(fieldRename: FieldRename.snake)
8 | class ModelImageMessage extends ModelBaseMessage {
9 | // base message
10 | @override
11 | final String id;
12 |
13 | @override
14 | final String userId;
15 |
16 | @override
17 | final int sequence;
18 |
19 | @override
20 | final DateTime displayDatetime;
21 |
22 | @override
23 | final bool forceNewBlock;
24 |
25 | /// The image urls of the message.
26 | final List imageUrls;
27 |
28 | /// The constructor of the image message.
29 | ModelImageMessage({
30 | required this.id,
31 | required this.userId,
32 | required this.sequence,
33 | required this.displayDatetime,
34 | required this.imageUrls,
35 | this.forceNewBlock = false,
36 | });
37 |
38 | // parsing
39 |
40 | /// The factory method for the image message from the json.
41 | factory ModelImageMessage.fromJson(Map json) =>
42 | _$ModelImageMessageFromJson(json);
43 |
44 | /// The method for the to json.
45 | Map toJson() => _$ModelImageMessageToJson(this);
46 | }
47 |
--------------------------------------------------------------------------------
/lib/models/image_message.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'image_message.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | ModelImageMessage _$ModelImageMessageFromJson(Map json) =>
10 | ModelImageMessage(
11 | id: json['id'] as String,
12 | userId: json['user_id'] as String,
13 | sequence: (json['sequence'] as num).toInt(),
14 | displayDatetime: DateTime.parse(json['display_datetime'] as String),
15 | imageUrls: (json['image_urls'] as List)
16 | .map((e) => e as String)
17 | .toList(),
18 | forceNewBlock: json['force_new_block'] as bool? ?? false,
19 | )..status = $enumDecode(_$ModelBaseMessageStatusEnumMap, json['status']);
20 |
21 | Map _$ModelImageMessageToJson(ModelImageMessage instance) =>
22 | {
23 | 'status': _$ModelBaseMessageStatusEnumMap[instance.status]!,
24 | 'id': instance.id,
25 | 'user_id': instance.userId,
26 | 'sequence': instance.sequence,
27 | 'display_datetime': instance.displayDatetime.toIso8601String(),
28 | 'force_new_block': instance.forceNewBlock,
29 | 'image_urls': instance.imageUrls,
30 | };
31 |
32 | const _$ModelBaseMessageStatusEnumMap = {
33 | ModelBaseMessageStatus.normal: 'normal',
34 | ModelBaseMessageStatus.failedToSend: 'failedToSend',
35 | ModelBaseMessageStatus.sending: 'sending',
36 | ModelBaseMessageStatus.sent: 'sent',
37 | ModelBaseMessageStatus.received: 'received',
38 | ModelBaseMessageStatus.read: 'read',
39 | };
40 |
--------------------------------------------------------------------------------
/lib/models/loading_indicator_message.dart:
--------------------------------------------------------------------------------
1 | import 'package:simple_chat/models/base_message.dart';
2 |
3 | /// The loading indicator message for the chat.
4 | class ModelLoadingIndicatorMessage extends ModelBaseMessage {
5 | /// The id of the message.
6 | @override
7 | final String id;
8 |
9 | /// The user id of the message.
10 | @override
11 | final String userId;
12 |
13 | /// The sequence of the message.
14 | @override
15 | final int sequence;
16 |
17 | /// The display datetime of the message.
18 | @override
19 | final DateTime displayDatetime;
20 |
21 | /// The constructor of the loading indicator message.
22 | ModelLoadingIndicatorMessage({
23 | required this.id,
24 | required this.userId,
25 | required this.sequence,
26 | required this.displayDatetime,
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/lib/models/message_send_output.dart:
--------------------------------------------------------------------------------
1 | import 'package:easy_asset_picker/picker/models/asset_image.dart';
2 |
3 | /// The output for the message send.
4 | class ChatMessageSendOutput {
5 | /// The message.
6 | final String message;
7 |
8 | /// The image files.
9 | final List imageFiles;
10 |
11 | /// The constructor of the message send output.
12 | ChatMessageSendOutput({
13 | required this.message,
14 | required this.imageFiles,
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/lib/models/text_message.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 | import 'package:simple_chat/models/base_message.dart';
3 |
4 | part 'text_message.g.dart';
5 |
6 | /// The text message for the chat.
7 | @JsonSerializable(fieldRename: FieldRename.snake)
8 | class ModelTextMessage extends ModelBaseMessage {
9 | // base message
10 | @override
11 | final String id;
12 |
13 | /// The user id of the message.
14 | @override
15 | final String userId;
16 |
17 | /// The sequence of the message.
18 | @override
19 | final int sequence;
20 |
21 | /// The display datetime of the message.
22 | @override
23 | final DateTime displayDatetime;
24 |
25 | /// The force new block of the message.
26 | @override
27 | final bool forceNewBlock;
28 |
29 | /// The text of the message.
30 | final String text;
31 |
32 | /// The constructor of the text message.
33 | ModelTextMessage({
34 | required this.id,
35 | required this.userId,
36 | required this.sequence,
37 | required this.displayDatetime,
38 | required this.text,
39 | this.forceNewBlock = false,
40 | });
41 |
42 | // parsing
43 |
44 | /// The factory method for the text message from the json.
45 | factory ModelTextMessage.fromJson(Map json) =>
46 | _$ModelTextMessageFromJson(json);
47 |
48 | /// The method for the to json.
49 | Map toJson() => _$ModelTextMessageToJson(this);
50 | }
51 |
--------------------------------------------------------------------------------
/lib/models/text_message.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'text_message.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | ModelTextMessage _$ModelTextMessageFromJson(Map json) =>
10 | ModelTextMessage(
11 | id: json['id'] as String,
12 | userId: json['user_id'] as String,
13 | sequence: (json['sequence'] as num).toInt(),
14 | displayDatetime: DateTime.parse(json['display_datetime'] as String),
15 | text: json['text'] as String,
16 | forceNewBlock: json['force_new_block'] as bool? ?? false,
17 | )..status = $enumDecode(_$ModelBaseMessageStatusEnumMap, json['status']);
18 |
19 | Map _$ModelTextMessageToJson(ModelTextMessage instance) =>
20 | {
21 | 'status': _$ModelBaseMessageStatusEnumMap[instance.status]!,
22 | 'id': instance.id,
23 | 'user_id': instance.userId,
24 | 'sequence': instance.sequence,
25 | 'display_datetime': instance.displayDatetime.toIso8601String(),
26 | 'force_new_block': instance.forceNewBlock,
27 | 'text': instance.text,
28 | };
29 |
30 | const _$ModelBaseMessageStatusEnumMap = {
31 | ModelBaseMessageStatus.normal: 'normal',
32 | ModelBaseMessageStatus.failedToSend: 'failedToSend',
33 | ModelBaseMessageStatus.sending: 'sending',
34 | ModelBaseMessageStatus.sent: 'sent',
35 | ModelBaseMessageStatus.received: 'received',
36 | ModelBaseMessageStatus.read: 'read',
37 | };
38 |
--------------------------------------------------------------------------------
/lib/models/user.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 | import 'package:simple_chat/simple_chat.dart';
3 |
4 | part 'user.g.dart';
5 |
6 | /// The user for the chat.
7 | @JsonSerializable(fieldRename: FieldRename.snake)
8 | class ModelUser extends ModelBaseUser {
9 | /// The id of the user.
10 | @override
11 | final String id;
12 |
13 | /// The avatar url of the user.
14 | @override
15 | final String avatarUrl;
16 |
17 | /// The flag for the current user.
18 | @override
19 | final bool isCurrentUser;
20 |
21 | /// The name of the user.
22 | @override
23 | final String name;
24 |
25 | /// The constructor of the user.
26 | ModelUser({
27 | required this.id,
28 | required this.name,
29 | required this.avatarUrl,
30 | required this.isCurrentUser,
31 | });
32 |
33 | // parsing
34 |
35 | /// The factory method for the user from the json.
36 | factory ModelUser.fromJson(Map json) =>
37 | _$ModelUserFromJson(json);
38 |
39 | /// The method for the to json.
40 | Map toJson() => _$ModelUserToJson(this);
41 | }
42 |
--------------------------------------------------------------------------------
/lib/models/user.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'user.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | ModelUser _$ModelUserFromJson(Map json) => ModelUser(
10 | id: json['id'] as String,
11 | name: json['name'] as String,
12 | avatarUrl: json['avatar_url'] as String,
13 | isCurrentUser: json['is_current_user'] as bool,
14 | );
15 |
16 | Map _$ModelUserToJson(ModelUser instance) => {
17 | 'id': instance.id,
18 | 'avatar_url': instance.avatarUrl,
19 | 'is_current_user': instance.isCurrentUser,
20 | 'name': instance.name,
21 | };
22 |
--------------------------------------------------------------------------------
/lib/simple_chat.dart:
--------------------------------------------------------------------------------
1 | export 'package:easy_asset_picker/picker/models/asset_image.dart';
2 |
3 | export 'controllers/chat_action_handler.dart';
4 | export 'controllers/chat_controller.dart';
5 | export 'models/base_message.dart';
6 | export 'models/base_user.dart';
7 | export 'models/image_message.dart';
8 | export 'models/message_send_output.dart';
9 | export 'models/text_message.dart';
10 | export 'models/user.dart';
11 | export 'stores/chat_store.dart';
12 | export 'theme/chat_theme.dart';
13 | export 'widgets/chat_view.dart';
14 |
15 | /// The package name of the chat package.
16 | const kChatPackage = 'simple_chat';
17 |
--------------------------------------------------------------------------------
/lib/stores/chat_store.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:easy_asset_picker/easy_asset_picker.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:image_picker/image_picker.dart';
6 | import 'package:logger/logger.dart'; // Added import for logger
7 | import 'package:mobx/mobx.dart';
8 | import 'package:simple_chat/controllers/chat_scroll_controller.dart';
9 | import 'package:simple_chat/models/loading_indicator_message.dart';
10 | import 'package:simple_chat/simple_chat.dart';
11 | import 'package:simple_chat/stores/sequential_map.dart';
12 | import 'package:uuid/uuid.dart';
13 |
14 | part 'chat_store.g.dart';
15 |
16 | /// The store for the chat.
17 | class ChatStore = ChatStoreBase with _$ChatStore;
18 |
19 | /// The base class for the chat store.
20 | abstract class ChatStoreBase with Store {
21 | /// The scroll controller for the chat.
22 | final ChatScrollController chatScrollController;
23 |
24 | /// The config for the chat.
25 | final ChatConfig config;
26 |
27 | /// The text editing controller for the chat.
28 | final TextEditingController textEditingController = TextEditingController();
29 |
30 | /// The focus node for the chat.
31 | final FocusNode focusNode = FocusNode();
32 |
33 | /// The constructor of the chat store.
34 | ChatStoreBase(
35 | this.chatScrollController,
36 | this.config,
37 | ) {
38 | _setup();
39 | }
40 |
41 | Future _setup() async {
42 | // setup
43 | focusNode.addListener(() {
44 | if (focusNode.hasFocus) {
45 | _isInputBoxFocused = true;
46 | } else {
47 | _isInputBoxFocused = false;
48 | }
49 | });
50 | }
51 |
52 | // observables
53 |
54 | /// The sequential message map for the chat.
55 | final sequentialMessageMap = SequentialMessageMap();
56 |
57 | /// The users for the chat.
58 | @readonly
59 | ObservableMap _users =
60 | ObservableMap.of({});
61 |
62 | /// The current user for the chat.
63 | @readonly
64 | ModelBaseUser? _currentUser;
65 |
66 | /// The flag for the input box focused.
67 | bool _isInputBoxFocused = false;
68 |
69 | // observables - images
70 |
71 | /// The image files for the chat.
72 | @readonly
73 | ObservableList _imageFiles =
74 | ObservableList.of([]);
75 |
76 | // observables - send message
77 |
78 | @readonly
79 | bool _isSending = false;
80 |
81 | @readonly
82 | int _readSequence = 0;
83 |
84 | @readonly
85 | bool _hasUnreadMessages = false;
86 |
87 | @readonly
88 | int _unreadMessagesCount = 0;
89 |
90 | /// The flag for the reach image selection limit.
91 | @computed
92 | bool get reachImageSelectionLimit =>
93 | _imageFiles.length >= config.imageMaxCount;
94 |
95 | // actions
96 |
97 | /// The action for the add message.
98 | @action
99 | Future addMessage({
100 | required ModelBaseMessage message,
101 | bool isInitial = false,
102 | }) async {
103 | final isAtBottom = chatScrollController.isAtBottom();
104 | sequentialMessageMap.upsert(message);
105 | unawaited(
106 | postMessageProcessing(
107 | isAtBottom: isAtBottom,
108 | isInitial: isInitial,
109 | newMessages: [message],
110 | ),
111 | );
112 | }
113 |
114 | /// The action for the add messages.
115 | @action
116 | Future addMessages({
117 | required List messages,
118 | bool isInitial = false,
119 | }) async {
120 | final isAtBottom = chatScrollController.isAtBottom();
121 | sequentialMessageMap.upsertAll(messages);
122 | unawaited(
123 | postMessageProcessing(
124 | isAtBottom: isAtBottom,
125 | isInitial: isInitial,
126 | newMessages: messages,
127 | ),
128 | );
129 | }
130 |
131 | /// The action for the post message processing.
132 | @action
133 | Future postMessageProcessing({
134 | required bool isAtBottom,
135 | required bool isInitial,
136 | required List newMessages,
137 | }) async {
138 | if (isAtBottom) {
139 | _readSequence = sequentialMessageMap.getHighestSequence();
140 | WidgetsBinding.instance.addPostFrameCallback((_) {
141 | if (isInitial) {
142 | chatScrollController.jumpToBottom();
143 | } else {
144 | chatScrollController.scrollToBottom();
145 | }
146 | });
147 | } else if (isInitial) {
148 | _readSequence = sequentialMessageMap.getHighestSequence();
149 | }
150 | updateUnreadStatus();
151 | }
152 |
153 | /// The action for the read message.
154 | @action
155 | Future readMessage({
156 | required ModelBaseMessage message,
157 | }) async {
158 | if (message.sequence > _readSequence) {
159 | _readSequence = message.sequence;
160 | await updateUnreadStatus();
161 | }
162 | }
163 |
164 | /// The action for the read all messages.
165 | @action
166 | Future readAllMessages() async {
167 | _readSequence = sequentialMessageMap.getHighestSequence();
168 | await updateUnreadStatus();
169 | }
170 |
171 | /// The action for the update unread status.
172 | @action
173 | Future updateUnreadStatus() async {
174 | final highestSequence = sequentialMessageMap.getHighestSequence();
175 | _hasUnreadMessages = _readSequence < highestSequence;
176 | _unreadMessagesCount = sequentialMessageMap.sequentialValues
177 | .where((message) => message.sequence > _readSequence)
178 | .length;
179 | }
180 |
181 | /// The action for the remove message.
182 | @action
183 | Future removeMessage({
184 | required ModelBaseMessage message,
185 | }) async {
186 | sequentialMessageMap.remove(message.id);
187 | }
188 |
189 | /// The action for the remove message by id.
190 | @action
191 | Future removeMessageById({
192 | required String messageId,
193 | }) async {
194 | sequentialMessageMap.remove(messageId);
195 | }
196 |
197 | /// The action for the remove messages.
198 | @action
199 | Future removeMessages({
200 | required List messages,
201 | }) async {
202 | for (var message in messages) {
203 | sequentialMessageMap.remove(message.id);
204 | }
205 | }
206 |
207 | /// The action for the clear all.
208 | @action
209 | Future clearAll() async {
210 | sequentialMessageMap.clearAll();
211 | }
212 |
213 | // send status
214 |
215 | /// The action for the update send status.
216 | @action
217 | Future updateSendStatus({
218 | required String messageId,
219 | required ModelBaseMessageStatus status,
220 | }) async {
221 | final message = sequentialMessageMap.getById(messageId);
222 | if (message != null) {
223 | message.status = status;
224 | sequentialMessageMap.upsert(message);
225 | }
226 | }
227 |
228 | // users
229 |
230 | /// The action for the add user.
231 | @action
232 | Future addUser({
233 | required ModelBaseUser user,
234 | }) async {
235 | _users[user.id] = user;
236 | if (user.isCurrentUser) {
237 | _currentUser = user;
238 | }
239 | }
240 |
241 | /// The action for the add users.
242 | @action
243 | Future addUsers({
244 | required List users,
245 | }) async {
246 | for (var user in users) {
247 | _users[user.id] = user;
248 | if (user.isCurrentUser) {
249 | _currentUser = user;
250 | }
251 | }
252 | }
253 |
254 | /// The loading indicator message id.
255 | final loadingIndicatorMessageId = const Uuid().v4();
256 |
257 | /// The action for the send message.
258 | @action
259 | Future sendMessage(
260 | {required Function(ChatMessageSendOutput output) onSend}) async {
261 | final output = ChatMessageSendOutput(
262 | message: textEditingController.text,
263 | imageFiles: _imageFiles.toList(),
264 | );
265 | if (config.loadingIndicatorType == LoadingIndicatorType.sendBtnLoading) {
266 | _isSending = true;
267 | try {
268 | await onSend(output);
269 | textEditingController.clear();
270 | _imageFiles.clear();
271 | } catch (e) {
272 | Logger().e('Error occurred: $e');
273 | }
274 | _isSending = false;
275 | } else {
276 | try {
277 | textEditingController.clear();
278 | _imageFiles.clear();
279 | onSend(output);
280 | } catch (e) {
281 | Logger().e('Error occurred: $e');
282 | }
283 | }
284 | }
285 |
286 | // actions - images
287 | bool _isTakingPhoto = false;
288 |
289 | /// The action for the take photo.
290 | @action
291 | Future takePhoto(BuildContext context) async {
292 | if (_isSending || _isTakingPhoto) {
293 | return;
294 | }
295 | _isTakingPhoto = true;
296 | final isAtBottom = chatScrollController.isAtBottom();
297 | final image = await ImagePicker().pickImage(source: ImageSource.camera);
298 | if (image != null) {
299 | final updated = _imageFiles.toList();
300 | updated.add(
301 | AssetImageInfo(
302 | path: image.path,
303 | ),
304 | );
305 | _imageFiles = ObservableList.of(updated);
306 | }
307 | if (isAtBottom) {
308 | WidgetsBinding.instance.addPostFrameCallback((_) {
309 | chatScrollController.scrollToBottom();
310 | });
311 | }
312 | _isTakingPhoto = false;
313 | }
314 |
315 | bool _isPickingImage = false;
316 |
317 | /// The action for the pick image.
318 | @action
319 | Future pickImage(BuildContext context) async {
320 | if (_isSending || _isPickingImage) {
321 | return;
322 | }
323 | _isPickingImage = true;
324 | final isAtBottom = chatScrollController.isAtBottom();
325 | final results = await showAssetPicker(
326 | context,
327 | config: AssetPickerConfig(
328 | maxSelection: config.imageMaxCount,
329 | selectIndicatorColor: context.coloredTheme.primary,
330 | loadingIndicatorColor: context.coloredTheme.primary,
331 | permissionDeniedText: config.photoPermissionDeniedText ??
332 | 'Please grant permission to access your photo library',
333 | permissionDeniedButtonText:
334 | config.photoPermissionDeniedButtonText ?? 'Open Settings',
335 | ),
336 | );
337 | _imageFiles = ObservableList.of(results ?? []);
338 | if (isAtBottom) {
339 | WidgetsBinding.instance.addPostFrameCallback((_) {
340 | chatScrollController.scrollToBottom();
341 | });
342 | }
343 | _isPickingImage = false;
344 | }
345 |
346 | /// The action for the remove image.
347 | @action
348 | Future removeImage({
349 | required AssetImageInfo image,
350 | }) async {
351 | if (_isSending) {
352 | return;
353 | }
354 | final updated = _imageFiles.toList();
355 | updated.remove(image);
356 | _imageFiles = ObservableList.of(updated);
357 | }
358 |
359 | // loading indicator
360 |
361 | /// The action for the show reply generating indicator.
362 | @action
363 | Future showReplyGeneratingIndicator() async {
364 | await addMessage(
365 | isInitial: false,
366 | message: ModelLoadingIndicatorMessage(
367 | id: loadingIndicatorMessageId,
368 | userId: '',
369 | sequence: (1 << 63) - 1,
370 | displayDatetime: DateTime.now(),
371 | ),
372 | );
373 | }
374 |
375 | /// The action for the hide reply generating indicator.
376 | @action
377 | Future hideReplyGeneratingIndicator() async {
378 | await removeMessageById(messageId: loadingIndicatorMessageId);
379 | }
380 |
381 | // public
382 |
383 | /// The method for the is message from current user.
384 | bool isMessageFromCurrentUser(ModelBaseMessage message) {
385 | return message.userId == _currentUser?.id;
386 | }
387 | }
388 |
--------------------------------------------------------------------------------
/lib/stores/chat_store.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'chat_store.dart';
4 |
5 | // **************************************************************************
6 | // StoreGenerator
7 | // **************************************************************************
8 |
9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
10 |
11 | mixin _$ChatStore on ChatStoreBase, Store {
12 | Computed? _$reachImageSelectionLimitComputed;
13 |
14 | @override
15 | bool get reachImageSelectionLimit => (_$reachImageSelectionLimitComputed ??=
16 | Computed(() => super.reachImageSelectionLimit,
17 | name: 'ChatStoreBase.reachImageSelectionLimit'))
18 | .value;
19 |
20 | late final _$_usersAtom =
21 | Atom(name: 'ChatStoreBase._users', context: context);
22 |
23 | ObservableMap get users {
24 | _$_usersAtom.reportRead();
25 | return super._users;
26 | }
27 |
28 | @override
29 | ObservableMap get _users => users;
30 |
31 | @override
32 | set _users(ObservableMap value) {
33 | _$_usersAtom.reportWrite(value, super._users, () {
34 | super._users = value;
35 | });
36 | }
37 |
38 | late final _$_currentUserAtom =
39 | Atom(name: 'ChatStoreBase._currentUser', context: context);
40 |
41 | ModelBaseUser? get currentUser {
42 | _$_currentUserAtom.reportRead();
43 | return super._currentUser;
44 | }
45 |
46 | @override
47 | ModelBaseUser? get _currentUser => currentUser;
48 |
49 | @override
50 | set _currentUser(ModelBaseUser? value) {
51 | _$_currentUserAtom.reportWrite(value, super._currentUser, () {
52 | super._currentUser = value;
53 | });
54 | }
55 |
56 | late final _$_isInputBoxFocusedAtom =
57 | Atom(name: 'ChatStoreBase._isInputBoxFocused', context: context);
58 |
59 | bool get isInputBoxFocused {
60 | _$_isInputBoxFocusedAtom.reportRead();
61 | return super._isInputBoxFocused;
62 | }
63 |
64 | @override
65 | bool get _isInputBoxFocused => isInputBoxFocused;
66 |
67 | @override
68 | set _isInputBoxFocused(bool value) {
69 | _$_isInputBoxFocusedAtom.reportWrite(value, super._isInputBoxFocused, () {
70 | super._isInputBoxFocused = value;
71 | });
72 | }
73 |
74 | late final _$_imageFilesAtom =
75 | Atom(name: 'ChatStoreBase._imageFiles', context: context);
76 |
77 | ObservableList get imageFiles {
78 | _$_imageFilesAtom.reportRead();
79 | return super._imageFiles;
80 | }
81 |
82 | @override
83 | ObservableList get _imageFiles => imageFiles;
84 |
85 | @override
86 | set _imageFiles(ObservableList value) {
87 | _$_imageFilesAtom.reportWrite(value, super._imageFiles, () {
88 | super._imageFiles = value;
89 | });
90 | }
91 |
92 | late final _$_isSendingAtom =
93 | Atom(name: 'ChatStoreBase._isSending', context: context);
94 |
95 | bool get isSending {
96 | _$_isSendingAtom.reportRead();
97 | return super._isSending;
98 | }
99 |
100 | @override
101 | bool get _isSending => isSending;
102 |
103 | @override
104 | set _isSending(bool value) {
105 | _$_isSendingAtom.reportWrite(value, super._isSending, () {
106 | super._isSending = value;
107 | });
108 | }
109 |
110 | late final _$_readSequenceAtom =
111 | Atom(name: 'ChatStoreBase._readSequence', context: context);
112 |
113 | int get readSequence {
114 | _$_readSequenceAtom.reportRead();
115 | return super._readSequence;
116 | }
117 |
118 | @override
119 | int get _readSequence => readSequence;
120 |
121 | @override
122 | set _readSequence(int value) {
123 | _$_readSequenceAtom.reportWrite(value, super._readSequence, () {
124 | super._readSequence = value;
125 | });
126 | }
127 |
128 | late final _$_hasUnreadMessagesAtom =
129 | Atom(name: 'ChatStoreBase._hasUnreadMessages', context: context);
130 |
131 | bool get hasUnreadMessages {
132 | _$_hasUnreadMessagesAtom.reportRead();
133 | return super._hasUnreadMessages;
134 | }
135 |
136 | @override
137 | bool get _hasUnreadMessages => hasUnreadMessages;
138 |
139 | @override
140 | set _hasUnreadMessages(bool value) {
141 | _$_hasUnreadMessagesAtom.reportWrite(value, super._hasUnreadMessages, () {
142 | super._hasUnreadMessages = value;
143 | });
144 | }
145 |
146 | late final _$_unreadMessagesCountAtom =
147 | Atom(name: 'ChatStoreBase._unreadMessagesCount', context: context);
148 |
149 | int get unreadMessagesCount {
150 | _$_unreadMessagesCountAtom.reportRead();
151 | return super._unreadMessagesCount;
152 | }
153 |
154 | @override
155 | int get _unreadMessagesCount => unreadMessagesCount;
156 |
157 | @override
158 | set _unreadMessagesCount(int value) {
159 | _$_unreadMessagesCountAtom.reportWrite(value, super._unreadMessagesCount,
160 | () {
161 | super._unreadMessagesCount = value;
162 | });
163 | }
164 |
165 | late final _$addMessageAsyncAction =
166 | AsyncAction('ChatStoreBase.addMessage', context: context);
167 |
168 | @override
169 | Future addMessage(
170 | {required ModelBaseMessage message, bool isInitial = false}) {
171 | return _$addMessageAsyncAction
172 | .run(() => super.addMessage(message: message, isInitial: isInitial));
173 | }
174 |
175 | late final _$addMessagesAsyncAction =
176 | AsyncAction('ChatStoreBase.addMessages', context: context);
177 |
178 | @override
179 | Future addMessages(
180 | {required List messages, bool isInitial = false}) {
181 | return _$addMessagesAsyncAction
182 | .run(() => super.addMessages(messages: messages, isInitial: isInitial));
183 | }
184 |
185 | late final _$postMessageProcessingAsyncAction =
186 | AsyncAction('ChatStoreBase.postMessageProcessing', context: context);
187 |
188 | @override
189 | Future postMessageProcessing(
190 | {required bool isAtBottom,
191 | required bool isInitial,
192 | required List newMessages}) {
193 | return _$postMessageProcessingAsyncAction.run(() => super
194 | .postMessageProcessing(
195 | isAtBottom: isAtBottom,
196 | isInitial: isInitial,
197 | newMessages: newMessages));
198 | }
199 |
200 | late final _$readMessageAsyncAction =
201 | AsyncAction('ChatStoreBase.readMessage', context: context);
202 |
203 | @override
204 | Future readMessage({required ModelBaseMessage message}) {
205 | return _$readMessageAsyncAction
206 | .run(() => super.readMessage(message: message));
207 | }
208 |
209 | late final _$readAllMessagesAsyncAction =
210 | AsyncAction('ChatStoreBase.readAllMessages', context: context);
211 |
212 | @override
213 | Future readAllMessages() {
214 | return _$readAllMessagesAsyncAction.run(() => super.readAllMessages());
215 | }
216 |
217 | late final _$updateUnreadStatusAsyncAction =
218 | AsyncAction('ChatStoreBase.updateUnreadStatus', context: context);
219 |
220 | @override
221 | Future updateUnreadStatus() {
222 | return _$updateUnreadStatusAsyncAction
223 | .run(() => super.updateUnreadStatus());
224 | }
225 |
226 | late final _$removeMessageAsyncAction =
227 | AsyncAction('ChatStoreBase.removeMessage', context: context);
228 |
229 | @override
230 | Future removeMessage({required ModelBaseMessage message}) {
231 | return _$removeMessageAsyncAction
232 | .run(() => super.removeMessage(message: message));
233 | }
234 |
235 | late final _$removeMessageByIdAsyncAction =
236 | AsyncAction('ChatStoreBase.removeMessageById', context: context);
237 |
238 | @override
239 | Future removeMessageById({required String messageId}) {
240 | return _$removeMessageByIdAsyncAction
241 | .run(() => super.removeMessageById(messageId: messageId));
242 | }
243 |
244 | late final _$removeMessagesAsyncAction =
245 | AsyncAction('ChatStoreBase.removeMessages', context: context);
246 |
247 | @override
248 | Future removeMessages({required List messages}) {
249 | return _$removeMessagesAsyncAction
250 | .run(() => super.removeMessages(messages: messages));
251 | }
252 |
253 | late final _$clearAllAsyncAction =
254 | AsyncAction('ChatStoreBase.clearAll', context: context);
255 |
256 | @override
257 | Future clearAll() {
258 | return _$clearAllAsyncAction.run(() => super.clearAll());
259 | }
260 |
261 | late final _$updateSendStatusAsyncAction =
262 | AsyncAction('ChatStoreBase.updateSendStatus', context: context);
263 |
264 | @override
265 | Future updateSendStatus(
266 | {required String messageId, required ModelBaseMessageStatus status}) {
267 | return _$updateSendStatusAsyncAction.run(
268 | () => super.updateSendStatus(messageId: messageId, status: status));
269 | }
270 |
271 | late final _$addUserAsyncAction =
272 | AsyncAction('ChatStoreBase.addUser', context: context);
273 |
274 | @override
275 | Future addUser({required ModelBaseUser user}) {
276 | return _$addUserAsyncAction.run(() => super.addUser(user: user));
277 | }
278 |
279 | late final _$addUsersAsyncAction =
280 | AsyncAction('ChatStoreBase.addUsers', context: context);
281 |
282 | @override
283 | Future addUsers({required List users}) {
284 | return _$addUsersAsyncAction.run(() => super.addUsers(users: users));
285 | }
286 |
287 | late final _$sendMessageAsyncAction =
288 | AsyncAction('ChatStoreBase.sendMessage', context: context);
289 |
290 | @override
291 | Future sendMessage(
292 | {required dynamic Function(ChatMessageSendOutput) onSend}) {
293 | return _$sendMessageAsyncAction
294 | .run(() => super.sendMessage(onSend: onSend));
295 | }
296 |
297 | late final _$takePhotoAsyncAction =
298 | AsyncAction('ChatStoreBase.takePhoto', context: context);
299 |
300 | @override
301 | Future takePhoto(BuildContext context) {
302 | return _$takePhotoAsyncAction.run(() => super.takePhoto(context));
303 | }
304 |
305 | late final _$pickImageAsyncAction =
306 | AsyncAction('ChatStoreBase.pickImage', context: context);
307 |
308 | @override
309 | Future pickImage(BuildContext context) {
310 | return _$pickImageAsyncAction.run(() => super.pickImage(context));
311 | }
312 |
313 | late final _$removeImageAsyncAction =
314 | AsyncAction('ChatStoreBase.removeImage', context: context);
315 |
316 | @override
317 | Future removeImage({required AssetImageInfo image}) {
318 | return _$removeImageAsyncAction.run(() => super.removeImage(image: image));
319 | }
320 |
321 | late final _$showReplyGeneratingIndicatorAsyncAction = AsyncAction(
322 | 'ChatStoreBase.showReplyGeneratingIndicator',
323 | context: context);
324 |
325 | @override
326 | Future showReplyGeneratingIndicator() {
327 | return _$showReplyGeneratingIndicatorAsyncAction
328 | .run(() => super.showReplyGeneratingIndicator());
329 | }
330 |
331 | late final _$hideReplyGeneratingIndicatorAsyncAction = AsyncAction(
332 | 'ChatStoreBase.hideReplyGeneratingIndicator',
333 | context: context);
334 |
335 | @override
336 | Future hideReplyGeneratingIndicator() {
337 | return _$hideReplyGeneratingIndicatorAsyncAction
338 | .run(() => super.hideReplyGeneratingIndicator());
339 | }
340 |
341 | @override
342 | String toString() {
343 | return '''
344 | reachImageSelectionLimit: ${reachImageSelectionLimit}
345 | ''';
346 | }
347 | }
348 |
--------------------------------------------------------------------------------
/lib/stores/sequential_map.dart:
--------------------------------------------------------------------------------
1 | import 'package:mobx/mobx.dart';
2 | import 'package:simple_chat/models/base_message.dart';
3 |
4 | part 'sequential_map.g.dart';
5 |
6 | /// The sequential message map.
7 | class SequentialMessageMap = SequentialMessageMapBase
8 | with _$SequentialMessageMap;
9 |
10 | /// The base class for the sequential message map.
11 | abstract class SequentialMessageMapBase with Store {
12 | /// The map for the messages.
13 | final ObservableMap _map =
14 | ObservableMap.of({});
15 |
16 | /// The order for the messages.
17 | final ObservableList _order = ObservableList.of([]);
18 |
19 | /// The message status map for the messages.
20 | @readonly
21 | ObservableMap _messageStatusMap =
22 | ObservableMap.of({});
23 |
24 | /// The computed for the sequential values.
25 | @computed
26 | ObservableList get sequentialValues =>
27 | ObservableList.of(_order.map((id) => _map[id]!));
28 |
29 | /// The computed for the sequential values reversed.
30 | @computed
31 | ObservableList get sequentialValuesReversed =>
32 | ObservableList.of(_order.reversed.map((id) => _map[id]!));
33 |
34 | /// The action for the get by id.
35 | @action
36 | ModelBaseMessage? getById(String id) => _map[id]; // Access by ID
37 |
38 | /// The action for the get highest sequence.
39 | @action
40 | int getHighestSequence() {
41 | if (_order.isEmpty) return 0;
42 | return _map[_order.first]?.sequence ?? 0;
43 | }
44 |
45 | /// The action for the upsert.
46 | @action
47 | void upsert(ModelBaseMessage msg) {
48 | if (!_map.containsKey(msg.id)) {
49 | // For new messages, add to order list
50 | _order.add(msg.id);
51 | }
52 | _map[msg.id] = msg;
53 | _messageStatusMap[msg.id] = msg.status;
54 | // Always resort to maintain correct order
55 | // This handles both new insertions and updates
56 | _order.sort((a, b) {
57 | final seqA = _map[a]?.sequence ?? 0;
58 | final seqB = _map[b]?.sequence ?? 0;
59 | return seqB.compareTo(seqA); // Descending order (newer messages first)
60 | });
61 | }
62 |
63 | /// The action for the upsert all.
64 | @action
65 | void upsertAll(List msgs) {
66 | // Skip if empty list
67 | if (msgs.isEmpty) return;
68 |
69 | // Update or add each message to the map
70 | for (final msg in msgs) {
71 | if (!_map.containsKey(msg.id)) {
72 | _order.add(msg.id);
73 | }
74 | _map[msg.id] = msg;
75 | _messageStatusMap[msg.id] = msg.status;
76 | }
77 |
78 | // Sort the entire _order list based on sequence numbers
79 | _order.sort((a, b) {
80 | final seqA = _map[a]?.sequence ?? 0;
81 | final seqB = _map[b]?.sequence ?? 0;
82 | return seqB.compareTo(seqA); // Descending order (newer messages first)
83 | });
84 | }
85 |
86 | /// The action for the remove.
87 | @action
88 | void remove(String id) {
89 | if (_map.containsKey(id)) {
90 | _map.remove(id); // Remove from the map
91 | _order.remove(id); // Remove from the sequential order
92 | _messageStatusMap.remove(id);
93 | }
94 | }
95 |
96 | /// The action for the clear all.
97 | @action
98 | void clearAll() {
99 | _map.clear();
100 | _order.clear();
101 | _messageStatusMap.clear();
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/lib/stores/sequential_map.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'sequential_map.dart';
4 |
5 | // **************************************************************************
6 | // StoreGenerator
7 | // **************************************************************************
8 |
9 | // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
10 |
11 | mixin _$SequentialMessageMap on SequentialMessageMapBase, Store {
12 | Computed>? _$sequentialValuesComputed;
13 |
14 | @override
15 | ObservableList get sequentialValues =>
16 | (_$sequentialValuesComputed ??=
17 | Computed>(
18 | () => super.sequentialValues,
19 | name: 'SequentialMessageMapBase.sequentialValues'))
20 | .value;
21 | Computed>?
22 | _$sequentialValuesReversedComputed;
23 |
24 | @override
25 | ObservableList get sequentialValuesReversed =>
26 | (_$sequentialValuesReversedComputed ??=
27 | Computed>(
28 | () => super.sequentialValuesReversed,
29 | name: 'SequentialMessageMapBase.sequentialValuesReversed'))
30 | .value;
31 |
32 | late final _$_messageStatusMapAtom = Atom(
33 | name: 'SequentialMessageMapBase._messageStatusMap', context: context);
34 |
35 | ObservableMap get messageStatusMap {
36 | _$_messageStatusMapAtom.reportRead();
37 | return super._messageStatusMap;
38 | }
39 |
40 | @override
41 | ObservableMap get _messageStatusMap =>
42 | messageStatusMap;
43 |
44 | @override
45 | set _messageStatusMap(ObservableMap value) {
46 | _$_messageStatusMapAtom.reportWrite(value, super._messageStatusMap, () {
47 | super._messageStatusMap = value;
48 | });
49 | }
50 |
51 | late final _$SequentialMessageMapBaseActionController =
52 | ActionController(name: 'SequentialMessageMapBase', context: context);
53 |
54 | @override
55 | ModelBaseMessage? getById(String id) {
56 | final _$actionInfo = _$SequentialMessageMapBaseActionController.startAction(
57 | name: 'SequentialMessageMapBase.getById');
58 | try {
59 | return super.getById(id);
60 | } finally {
61 | _$SequentialMessageMapBaseActionController.endAction(_$actionInfo);
62 | }
63 | }
64 |
65 | @override
66 | int getHighestSequence() {
67 | final _$actionInfo = _$SequentialMessageMapBaseActionController.startAction(
68 | name: 'SequentialMessageMapBase.getHighestSequence');
69 | try {
70 | return super.getHighestSequence();
71 | } finally {
72 | _$SequentialMessageMapBaseActionController.endAction(_$actionInfo);
73 | }
74 | }
75 |
76 | @override
77 | void upsert(ModelBaseMessage msg) {
78 | final _$actionInfo = _$SequentialMessageMapBaseActionController.startAction(
79 | name: 'SequentialMessageMapBase.upsert');
80 | try {
81 | return super.upsert(msg);
82 | } finally {
83 | _$SequentialMessageMapBaseActionController.endAction(_$actionInfo);
84 | }
85 | }
86 |
87 | @override
88 | void upsertAll(List msgs) {
89 | final _$actionInfo = _$SequentialMessageMapBaseActionController.startAction(
90 | name: 'SequentialMessageMapBase.upsertAll');
91 | try {
92 | return super.upsertAll(msgs);
93 | } finally {
94 | _$SequentialMessageMapBaseActionController.endAction(_$actionInfo);
95 | }
96 | }
97 |
98 | @override
99 | void remove(String id) {
100 | final _$actionInfo = _$SequentialMessageMapBaseActionController.startAction(
101 | name: 'SequentialMessageMapBase.remove');
102 | try {
103 | return super.remove(id);
104 | } finally {
105 | _$SequentialMessageMapBaseActionController.endAction(_$actionInfo);
106 | }
107 | }
108 |
109 | @override
110 | void clearAll() {
111 | final _$actionInfo = _$SequentialMessageMapBaseActionController.startAction(
112 | name: 'SequentialMessageMapBase.clearAll');
113 | try {
114 | return super.clearAll();
115 | } finally {
116 | _$SequentialMessageMapBaseActionController.endAction(_$actionInfo);
117 | }
118 | }
119 |
120 | @override
121 | String toString() {
122 | return '''
123 | sequentialValues: ${sequentialValues},
124 | sequentialValuesReversed: ${sequentialValuesReversed}
125 | ''';
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/lib/theme/chat_theme.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | /// The extension for the themed context.
4 | extension ThemedContextExtension on BuildContext {
5 | /// The method for the get the colored theme.
6 | ChatColorThemeData get coloredTheme {
7 | final brightness = MediaQuery.of(this).platformBrightness;
8 | final chatTheme = ChatTheme.of(this);
9 | return brightness == Brightness.light ? chatTheme.light : chatTheme.dark;
10 | }
11 |
12 | /// The method for the get the layout theme.
13 | ChatLayoutThemeData get layoutTheme {
14 | final chatTheme = ChatTheme.of(this);
15 | return chatTheme.layout;
16 | }
17 | }
18 |
19 | /// The theme for the chat.
20 | class ChatTheme extends InheritedWidget {
21 | /// The constructor for the chat theme.
22 | const ChatTheme({
23 | super.key,
24 | required this.data,
25 | required super.child,
26 | });
27 |
28 | /// The data for the chat theme.
29 | final ChatThemeData data;
30 |
31 | /// The method for the of.
32 | static ChatThemeData of(BuildContext context) {
33 | final ChatTheme? theme =
34 | context.dependOnInheritedWidgetOfExactType();
35 | return theme?.data ??
36 | ChatThemeData(
37 | light: ChatColorThemeData.light,
38 | dark: ChatColorThemeData.dark,
39 | );
40 | }
41 |
42 | /// The method for the update should notify.
43 | @override
44 | bool updateShouldNotify(ChatTheme oldWidget) {
45 | return data != oldWidget.data;
46 | }
47 | }
48 |
49 | /// The data for the chat theme.
50 | class ChatThemeData {
51 | /// The constructor for the chat theme data.
52 | ChatThemeData({
53 | required this.light,
54 | required this.dark,
55 | this.layout = const ChatLayoutThemeData(),
56 | });
57 |
58 | /// The light color theme data.
59 | ChatColorThemeData light;
60 |
61 | /// The dark color theme data.
62 | ChatColorThemeData dark;
63 |
64 | /// The layout theme data.
65 | ChatLayoutThemeData layout;
66 | }
67 |
68 | /// The color theme data for the chat.
69 | class ChatColorThemeData {
70 | /// The light color theme data.
71 | static ChatColorThemeData get light => ChatColorThemeData();
72 |
73 | /// The dark color theme data.
74 | static ChatColorThemeData get dark => ChatColorThemeData();
75 |
76 | /// The background color.
77 | final Color backgroundColor;
78 |
79 | /// The input box color.
80 | final Color inputBoxColor;
81 |
82 | /// The my message color.
83 | final Color myMessageColor;
84 |
85 | /// The other message color.
86 | final Color otherMessageColor;
87 |
88 | /// The primary color.
89 | final Color primary;
90 |
91 | /// The unread indicator background color.
92 | final Color unreadIndicatorBackgroundColor;
93 |
94 | /// The sending indicator color.
95 | final Color sendingIndicatorColor;
96 |
97 | /// The constructor for the chat color theme data.
98 | ChatColorThemeData({
99 | this.backgroundColor = const Color(0xFFF5F5F5),
100 | this.inputBoxColor = Colors.white,
101 | this.myMessageColor = const Color(0xFFE5E5EA),
102 | this.otherMessageColor = Colors.white,
103 | this.primary = const Color(0xFFF86526),
104 | this.unreadIndicatorBackgroundColor = Colors.white,
105 | this.sendingIndicatorColor = const Color(0xFFF86526),
106 | });
107 |
108 | /// The method for the copy with.
109 | ChatColorThemeData copyWith({
110 | Color? backgroundColor,
111 | }) {
112 | return ChatColorThemeData(
113 | backgroundColor: backgroundColor ?? this.backgroundColor,
114 | );
115 | }
116 | }
117 |
118 | /// The layout theme data for the chat.
119 | class ChatLayoutThemeData {
120 | /// The constructor for the chat layout theme data.
121 | const ChatLayoutThemeData({
122 | this.chatViewPadding = const EdgeInsets.all(16),
123 | this.userAvatarSize = 36,
124 | this.avatarAndMessageSpacing = 8,
125 | this.failedToSendTextStyle = const TextStyle(
126 | fontSize: 11,
127 | color: Color(0xFFFF3B30),
128 | fontWeight: FontWeight.w400,
129 | height: 1.545,
130 | ),
131 | });
132 |
133 | /// The chat view padding.
134 | final EdgeInsets chatViewPadding;
135 |
136 | /// The user avatar size.
137 | final double userAvatarSize;
138 |
139 | /// The avatar and message spacing.
140 | final double avatarAndMessageSpacing;
141 |
142 | /// The failed to send text style.
143 | final TextStyle failedToSendTextStyle;
144 | }
145 |
--------------------------------------------------------------------------------
/lib/widgets/chat_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_mobx/flutter_mobx.dart';
3 | import 'package:flutter_svg/svg.dart';
4 | import 'package:simple_chat/models/loading_indicator_message.dart';
5 | import 'package:simple_chat/simple_chat.dart';
6 | import 'package:simple_chat/widgets/input/input_box.dart';
7 | import 'package:simple_chat/widgets/messages/unsupport_message_item.dart';
8 | import 'package:simple_chat/widgets/users/user_avatar.dart';
9 | import 'package:visibility_detector/visibility_detector.dart';
10 |
11 | /// The chat view.
12 | class ChatView extends StatefulWidget {
13 | /// The theme for the chat.
14 | late final ChatThemeData theme;
15 |
16 | /// The controller for the chat.
17 | late final ChatController controller;
18 |
19 | /// The constructor for the chat view.
20 | final Widget? toolbar;
21 |
22 | /// The constructor for the chat view.
23 | ChatView({
24 | super.key,
25 | ChatThemeData? theme,
26 | ChatController? controller,
27 | this.toolbar,
28 | }) {
29 | this.theme = theme ??
30 | ChatThemeData(
31 | light: ChatColorThemeData(),
32 | dark: ChatColorThemeData(),
33 | );
34 | this.controller = controller ?? ChatController();
35 | }
36 |
37 | /// The method for the create state.
38 | @override
39 | State createState() {
40 | return _ChatViewState();
41 | }
42 | }
43 |
44 | class _ChatViewState extends State {
45 | late final store = widget.controller.store;
46 | @override
47 | void initState() {
48 | super.initState();
49 | }
50 |
51 | void _dismissKeyboard() {
52 | store.focusNode.unfocus();
53 | }
54 |
55 | @override
56 | Widget build(BuildContext context) {
57 | final view = ChatTheme(
58 | data: ChatThemeData(
59 | light: widget.theme.light,
60 | dark: widget.theme.dark,
61 | ),
62 | child: Builder(
63 | builder: (context) => Container(
64 | color: context.coloredTheme.backgroundColor,
65 | child: Column(
66 | children: [
67 | Expanded(
68 | child: Stack(
69 | children: [
70 | GestureDetector(
71 | onTap: _dismissKeyboard,
72 | child: _buildMessageList(context),
73 | ),
74 | Align(
75 | alignment: Alignment.bottomCenter,
76 | child: _buildUnreadIndicator(context),
77 | )
78 | ],
79 | ),
80 | ),
81 | if (widget.toolbar != null) widget.toolbar!,
82 | InputBox(
83 | controller: widget.controller,
84 | textEditingController: store.textEditingController,
85 | focusNode: store.focusNode,
86 | onSend: () {
87 | store.sendMessage(
88 | onSend: (output) async {
89 | await widget.controller.actionHandler?.onSendMessage
90 | ?.call(output);
91 | },
92 | );
93 | },
94 | onCameraTap: () async {
95 | if (store.imageFiles.length >=
96 | widget.controller.config.imageMaxCount) {
97 | return;
98 | }
99 | await store.takePhoto(context);
100 | store.focusNode.requestFocus();
101 | },
102 | onAlbumTap: () async {
103 | if (store.imageFiles.length >=
104 | widget.controller.config.imageMaxCount) {
105 | return;
106 | }
107 | _dismissKeyboard();
108 | await store.pickImage(context);
109 | store.focusNode.requestFocus();
110 | },
111 | onImageTap: (imageFile) {
112 | widget.controller.actionHandler?.onImageThumbnailTap
113 | ?.call(imageFile);
114 | },
115 | onImageRemove: (imageFile) {
116 | store.removeImage(image: imageFile);
117 | },
118 | ),
119 | ],
120 | ),
121 | ),
122 | ),
123 | );
124 | return view;
125 | }
126 |
127 | Widget _buildMessageList(BuildContext context) {
128 | return Observer(
129 | builder: (context) {
130 | final sequentialMessageMap = store.sequentialMessageMap;
131 | final messages = sequentialMessageMap.sequentialValuesReversed;
132 | return Align(
133 | alignment: Alignment.topCenter,
134 | child: ListView.separated(
135 | physics: const ClampingScrollPhysics(),
136 | reverse: false,
137 | shrinkWrap: true,
138 | keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
139 | controller: widget.controller.chatScrollController.controller,
140 | padding: EdgeInsets.only(
141 | top: context.layoutTheme.chatViewPadding.top,
142 | bottom: context.layoutTheme.chatViewPadding.bottom,
143 | ),
144 | cacheExtent: 1000,
145 | separatorBuilder: (context, index) {
146 | final currentMessage = messages[index];
147 | final nextMessage =
148 | index + 1 < messages.length ? messages[index + 1] : null;
149 | final isSameUser = nextMessage != null &&
150 | currentMessage.userId == nextMessage.userId;
151 | final forceNewBlock = nextMessage?.forceNewBlock ?? false;
152 | return SizedBox(height: isSameUser && !forceNewBlock ? 4 : 16);
153 | },
154 | itemCount: messages.length,
155 | itemBuilder: (context, index) {
156 | return Observer(
157 | builder: (context) {
158 | final message = messages[index];
159 | final hideUserAvatar =
160 | message is ModelLoadingIndicatorMessage;
161 | final previousMessage =
162 | index - 1 >= 0 ? messages[index - 1] : null;
163 | final isMessageFromCurrentUser =
164 | store.isMessageFromCurrentUser(message);
165 | final isSameUser = previousMessage != null &&
166 | message.userId == previousMessage.userId;
167 | final updatedStatus =
168 | store.sequentialMessageMap.messageStatusMap[message.id];
169 |
170 | final builder = widget.controller.viewFactory.buildFor(
171 | context,
172 | message: message,
173 | isMessageFromCurrentUser: isMessageFromCurrentUser,
174 | );
175 | final messageItem = builder ??
176 | UnsupportMessageItem(
177 | isCurrentUser: isMessageFromCurrentUser);
178 | final user = store.users[message.userId];
179 | final userAvatar = isSameUser && !message.forceNewBlock
180 | ? SizedBox(width: context.layoutTheme.userAvatarSize)
181 | : UserAvatar(
182 | user: user,
183 | onTap: () {
184 | widget.controller.actionHandler?.onUserAvatarTap
185 | ?.call(user);
186 | },
187 | );
188 | const avatarMessageSpacing = 8.0;
189 |
190 | // Wrap messageItem with Flexible widget
191 | final flexibleMessageItem = Flexible(
192 | child: GestureDetector(
193 | onTap: () => widget.controller.actionHandler?.onMessageTap
194 | ?.call(message),
195 | child: Stack(
196 | clipBehavior: Clip.none,
197 | children: [
198 | messageItem,
199 | if (updatedStatus == ModelBaseMessageStatus.sending)
200 | Positioned(
201 | width: 12,
202 | height: 12,
203 | bottom: 0,
204 | right: isMessageFromCurrentUser ? -14 : null,
205 | left: isMessageFromCurrentUser ? null : -14,
206 | child: CircularProgressIndicator(
207 | color:
208 | context.coloredTheme.sendingIndicatorColor,
209 | strokeWidth: 2,
210 | ),
211 | ),
212 | ],
213 | ),
214 | ),
215 | );
216 |
217 | Widget contentView;
218 | List children;
219 | if (message.showAvatarAndPaddings) {
220 | children = [
221 | if (widget.controller.config.messageAlignment ==
222 | MessageAlignment.center)
223 | SizedBox(
224 | width: avatarMessageSpacing +
225 | context.layoutTheme.userAvatarSize),
226 | flexibleMessageItem,
227 | if (!hideUserAvatar)
228 | const SizedBox(width: avatarMessageSpacing),
229 | if (!hideUserAvatar) userAvatar,
230 | ];
231 | } else {
232 | children = [
233 | flexibleMessageItem,
234 | ];
235 | }
236 | if (isMessageFromCurrentUser) {
237 | contentView = Row(
238 | mainAxisAlignment: MainAxisAlignment.end,
239 | crossAxisAlignment: CrossAxisAlignment.start,
240 | children: children,
241 | );
242 | } else {
243 | contentView = Row(
244 | mainAxisAlignment: MainAxisAlignment.start,
245 | crossAxisAlignment: CrossAxisAlignment.start,
246 | children: children.reversed.toList(),
247 | );
248 | }
249 | Widget contentViewWithStatus = contentView;
250 |
251 | final avatarPadding =
252 | avatarMessageSpacing + context.layoutTheme.userAvatarSize;
253 | Widget? statusWidget;
254 | switch (updatedStatus) {
255 | case ModelBaseMessageStatus.failedToSend:
256 | statusWidget = GestureDetector(
257 | onTap: () => widget
258 | .controller.actionHandler?.onMessageTap
259 | ?.call(message),
260 | child: Text(
261 | widget.controller.config.failedToSendText ??
262 | 'Failed to send',
263 | style: context.layoutTheme.failedToSendTextStyle,
264 | ),
265 | );
266 | break;
267 | default:
268 | break;
269 | }
270 |
271 | if (statusWidget != null) {
272 | contentViewWithStatus = Column(
273 | mainAxisSize: MainAxisSize.min,
274 | crossAxisAlignment: isMessageFromCurrentUser
275 | ? CrossAxisAlignment.end
276 | : CrossAxisAlignment.start,
277 | children: [
278 | contentView,
279 | const SizedBox(height: 2),
280 | Padding(
281 | padding: isMessageFromCurrentUser
282 | ? EdgeInsets.only(right: avatarPadding)
283 | : EdgeInsets.only(left: avatarPadding),
284 | child: statusWidget,
285 | ),
286 | ],
287 | );
288 | }
289 | return VisibilityDetector(
290 | key: message.widgetKey,
291 | onVisibilityChanged: (visibility) {
292 | if (visibility.visibleFraction == 1) {
293 | store.readMessage(message: message);
294 | }
295 | },
296 | child: Padding(
297 | padding: message.customContainerPadding ??
298 | EdgeInsets.only(
299 | left: context.layoutTheme.chatViewPadding.left,
300 | right: context.layoutTheme.chatViewPadding.right,
301 | ),
302 | child: contentViewWithStatus,
303 | ),
304 | );
305 | },
306 | );
307 | },
308 | ),
309 | );
310 | },
311 | );
312 | }
313 |
314 | Widget _buildUnreadIndicator(BuildContext context) {
315 | return Observer(
316 | builder: (context) {
317 | if (store.hasUnreadMessages) {
318 | return GestureDetector(
319 | onTap: () {
320 | widget.controller.scrollToBottom();
321 | },
322 | child: widget.controller.config.showUnreadCount
323 | ? IntrinsicWidth(
324 | child: Container(
325 | height: 32,
326 | margin: const EdgeInsets.only(bottom: 8),
327 | padding: const EdgeInsets.symmetric(horizontal: 12),
328 | decoration: BoxDecoration(
329 | color:
330 | context.coloredTheme.unreadIndicatorBackgroundColor,
331 | borderRadius: BorderRadius.circular(16),
332 | boxShadow: [
333 | BoxShadow(
334 | color: Colors.black.withValues(alpha: 0.2),
335 | blurRadius: 8,
336 | ),
337 | ],
338 | ),
339 | clipBehavior: Clip.hardEdge,
340 | alignment: Alignment.center,
341 | child: Row(
342 | children: [
343 | Text(
344 | '${store.unreadMessagesCount > 99 ? '99+' : store.unreadMessagesCount}',
345 | style: Theme.of(context)
346 | .textTheme
347 | .bodyMedium
348 | ?.copyWith(
349 | fontWeight: FontWeight.w600,
350 | ),
351 | ),
352 | const SizedBox(width: 4),
353 | SvgPicture.asset(
354 | 'assets/svg/chat/arrow_down.svg',
355 | width: 16,
356 | height: 16,
357 | package: kChatPackage,
358 | ),
359 | ],
360 | ),
361 | ),
362 | )
363 | : Container(
364 | width: 32,
365 | height: 32,
366 | margin: const EdgeInsets.only(bottom: 8),
367 | decoration: BoxDecoration(
368 | color:
369 | context.coloredTheme.unreadIndicatorBackgroundColor,
370 | shape: BoxShape.circle,
371 | boxShadow: [
372 | BoxShadow(
373 | color: Colors.black.withValues(alpha: 0.2),
374 | blurRadius: 8,
375 | ),
376 | ],
377 | ),
378 | clipBehavior: Clip.hardEdge,
379 | alignment: Alignment.center,
380 | child: SvgPicture.asset(
381 | 'assets/svg/chat/arrow_down.svg',
382 | width: 24,
383 | height: 24,
384 | package: kChatPackage,
385 | ),
386 | ),
387 | );
388 | }
389 | return const SizedBox.shrink();
390 | },
391 | );
392 | }
393 | }
394 |
--------------------------------------------------------------------------------
/lib/widgets/input/input_box.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_mobx/flutter_mobx.dart';
3 | import 'package:simple_chat/simple_chat.dart';
4 | import 'package:simple_chat/widgets/input/input_box_image_item.dart';
5 | import 'package:simple_chat/widgets/input/input_box_text_field.dart';
6 |
7 | /// The input box for the chat.
8 | class InputBox extends StatelessWidget {
9 | /// The controller for the chat.
10 | final ChatController controller;
11 |
12 | /// The text editing controller.
13 | final TextEditingController textEditingController;
14 |
15 | /// The focus node.
16 | final FocusNode focusNode;
17 |
18 | /// The on send callback.
19 | final Function() onSend;
20 |
21 | /// The on camera tap callback.
22 | final Function() onCameraTap;
23 |
24 | /// The on album tap callback.
25 | final Function() onAlbumTap;
26 |
27 | /// The on image tap callback.
28 | final Function(AssetImageInfo) onImageTap;
29 |
30 | /// The on image remove callback.
31 | final Function(AssetImageInfo) onImageRemove;
32 |
33 | /// The store for the chat.
34 | late final ChatStore store;
35 |
36 | /// The constructor for the input box.
37 | InputBox({
38 | super.key,
39 | required this.controller,
40 | required this.textEditingController,
41 | required this.focusNode,
42 | required this.onSend,
43 | required this.onCameraTap,
44 | required this.onAlbumTap,
45 | required this.onImageTap,
46 | required this.onImageRemove,
47 | }) {
48 | store = controller.store;
49 | }
50 |
51 | /// The input box horizontal margin.
52 | final inputBoxHorizontalMargin = 16.0;
53 |
54 | @override
55 | Widget build(BuildContext context) {
56 | return Container(
57 | padding: EdgeInsets.only(
58 | left: inputBoxHorizontalMargin,
59 | right: inputBoxHorizontalMargin,
60 | top: 8.0,
61 | bottom: 8.0 + MediaQuery.of(context).padding.bottom,
62 | ),
63 | decoration: BoxDecoration(
64 | color: context.coloredTheme.inputBoxColor,
65 | border: Border(
66 | top: BorderSide(
67 | color: const Color(0xFF3C3C43).withValues(alpha: 0.1),
68 | width: 0.5,
69 | ),
70 | ),
71 | ),
72 | child: Column(
73 | mainAxisSize: MainAxisSize.min,
74 | crossAxisAlignment: CrossAxisAlignment.stretch,
75 | children: [
76 | InputBoxTextField(
77 | controller: controller,
78 | inputBoxHorizontalMargin: inputBoxHorizontalMargin,
79 | textEditingController: textEditingController,
80 | focusNode: focusNode,
81 | onSend: onSend,
82 | onCameraTap: onCameraTap,
83 | onAlbumTap: onAlbumTap,
84 | ),
85 | Observer(
86 | builder: (context) {
87 | final imageFiles = store.imageFiles;
88 | if (imageFiles.isNotEmpty) {
89 | return Padding(
90 | padding: const EdgeInsets.only(top: 8),
91 | child: Wrap(
92 | clipBehavior: Clip.none,
93 | spacing: 8.0,
94 | runSpacing: 8.0,
95 | alignment: WrapAlignment.start,
96 | children: [
97 | for (var imageFile in imageFiles)
98 | InputBoxImageItem(
99 | imageFile: imageFile,
100 | onTap: onImageTap,
101 | onRemove: onImageRemove,
102 | disabled: store.isSending,
103 | ),
104 | ],
105 | ),
106 | );
107 | }
108 | return const SizedBox.shrink();
109 | },
110 | ),
111 | ],
112 | ),
113 | );
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/lib/widgets/input/input_box_image_item.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:easy_asset_picker/picker/models/asset_image.dart';
4 | import 'package:flutter/material.dart';
5 |
6 | /// The input box image item.
7 | class InputBoxImageItem extends StatelessWidget {
8 | /// The image file.
9 | final AssetImageInfo imageFile;
10 |
11 | /// The size of the image.
12 | final double size;
13 |
14 | /// The disabled flag.
15 | final bool disabled;
16 |
17 | /// The on tap callback.
18 | final void Function(AssetImageInfo) onTap;
19 |
20 | /// The on remove callback.
21 | final void Function(AssetImageInfo) onRemove;
22 |
23 | /// The constructor for the input box image item.
24 | const InputBoxImageItem({
25 | super.key,
26 | required this.imageFile,
27 | this.size = 60,
28 | required this.onTap,
29 | required this.onRemove,
30 | this.disabled = false,
31 | });
32 |
33 | @override
34 | Widget build(BuildContext context) {
35 | return ClipRRect(
36 | borderRadius: BorderRadius.circular(12),
37 | child: GestureDetector(
38 | onTap: () => onTap(imageFile),
39 | child: SizedBox(
40 | width: size,
41 | height: size,
42 | child: Stack(
43 | children: [
44 | Image.file(
45 | File(imageFile.path),
46 | fit: BoxFit.cover,
47 | width: size,
48 | height: size,
49 | ),
50 | if (!disabled)
51 | Positioned(
52 | top: 3,
53 | right: 3,
54 | child: GestureDetector(
55 | onTap: () => onRemove(imageFile),
56 | child: Container(
57 | width: 18,
58 | height: 18,
59 | decoration: const BoxDecoration(
60 | color: Colors.black,
61 | shape: BoxShape.circle,
62 | ),
63 | child: const Icon(
64 | Icons.close,
65 | color: Colors.white,
66 | size: 12,
67 | ),
68 | ),
69 | ),
70 | ),
71 | ],
72 | ),
73 | ),
74 | ),
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/lib/widgets/input/input_box_text_field.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_mobx/flutter_mobx.dart';
5 | import 'package:flutter_svg/svg.dart';
6 | import 'package:simple_chat/simple_chat.dart';
7 | import 'package:simple_chat/widgets/input/send_msg_btn.dart';
8 |
9 | /// The input box text field.
10 | class InputBoxTextField extends StatefulWidget {
11 | /// The input box horizontal margin.
12 | final double inputBoxHorizontalMargin;
13 |
14 | /// The controller for the chat.
15 | final ChatController controller;
16 |
17 | /// The text editing controller.
18 | final TextEditingController textEditingController;
19 |
20 | /// The focus node.
21 | final FocusNode focusNode;
22 |
23 | /// The on send callback.
24 | final Function() onSend;
25 |
26 | /// The on camera tap callback.
27 | final Function() onCameraTap;
28 |
29 | /// The on album tap callback.
30 | final Function() onAlbumTap;
31 |
32 | /// The constructor for the input box text field.
33 | const InputBoxTextField({
34 | super.key,
35 | this.inputBoxHorizontalMargin = 16.0,
36 | required this.controller,
37 | required this.textEditingController,
38 | required this.focusNode,
39 | required this.onSend,
40 | required this.onCameraTap,
41 | required this.onAlbumTap,
42 | });
43 |
44 | @override
45 | State createState() => _InputBoxTextFieldState();
46 | }
47 |
48 | class _InputBoxTextFieldState extends State {
49 | late final store = widget.controller.store;
50 | final textFieldKey = GlobalKey();
51 | final textFieldStyle = const TextStyle(
52 | color: Colors.black,
53 | fontSize: 16,
54 | height: 1.5,
55 | fontWeight: FontWeight.w400,
56 | );
57 | final textFieldHorizontalPadding = 16.0;
58 | final cursorWidth = 2.0;
59 |
60 | @override
61 | void initState() {
62 | super.initState();
63 | widget.textEditingController.addListener(() {
64 | setState(() {});
65 | });
66 | }
67 |
68 | @override
69 | Widget build(BuildContext context) {
70 | const inputBoxHeight = 40.0;
71 | final double cameraIconWidth =
72 | widget.controller.config.imageMaxCount > 0 ? 24.0 : 0;
73 | final double cameraIconRightPadding =
74 | widget.controller.config.imageMaxCount > 0 ? 16.0 : 0;
75 | final double albumIconWidth =
76 | widget.controller.config.imageMaxCount > 0 ? 24.0 : 0;
77 | final double albumIconRightPadding =
78 | widget.controller.config.imageMaxCount > 0 ? 16.0 : 0;
79 | const double sendMsgBtnWidth = 32.0;
80 | const double sendMsgBtnRightPadding = 4.0;
81 | final double buttonBoxWidth = cameraIconWidth +
82 | albumIconWidth +
83 | sendMsgBtnWidth +
84 | cameraIconRightPadding +
85 | albumIconRightPadding +
86 | sendMsgBtnRightPadding;
87 | final textFieldMinWidth = MediaQuery.of(context).size.width -
88 | widget.inputBoxHorizontalMargin * 2 -
89 | buttonBoxWidth;
90 | return Container(
91 | decoration: BoxDecoration(
92 | color: const Color(0xFFF5F5F5),
93 | borderRadius: BorderRadius.circular(inputBoxHeight / 2),
94 | ),
95 | alignment: Alignment.center,
96 | child: LayoutBuilder(
97 | builder: (context, outerConstraint) {
98 | final textFieldWidth = _calculateTextFieldWidth(context);
99 | bool isAloneInRow = textFieldWidth > textFieldMinWidth;
100 | List wrapChildren = [
101 | IntrinsicWidth(
102 | child: ConstrainedBox(
103 | constraints: BoxConstraints(
104 | minWidth: textFieldMinWidth,
105 | maxWidth: isAloneInRow ? double.infinity : textFieldMinWidth,
106 | ),
107 | child: TextField(
108 | key: textFieldKey,
109 | cursorWidth: cursorWidth,
110 | cursorColor: context.coloredTheme.primary,
111 | controller: widget.textEditingController,
112 | style: textFieldStyle,
113 | autofocus: false,
114 | textAlign: TextAlign.left,
115 | decoration: InputDecoration(
116 | border: InputBorder.none,
117 | hintText: widget.controller.config.inputBoxHintText,
118 | hintStyle: textFieldStyle.copyWith(
119 | color: const Color(0xFF3C3C3C).withValues(alpha: 0.3),
120 | ),
121 | contentPadding: EdgeInsets.symmetric(
122 | horizontal: textFieldHorizontalPadding,
123 | vertical: 8,
124 | ),
125 | isDense: true,
126 | ),
127 | focusNode: widget.focusNode,
128 | maxLines: isAloneInRow ? 4 : 1,
129 | minLines: 1,
130 | keyboardType: TextInputType.multiline,
131 | ),
132 | ),
133 | ),
134 | ];
135 | final buttonBox = Container(
136 | width: buttonBoxWidth,
137 | height: inputBoxHeight,
138 | alignment: Alignment.center,
139 | child: Row(
140 | crossAxisAlignment: CrossAxisAlignment.center,
141 | mainAxisSize: MainAxisSize.min,
142 | children: [
143 | if (widget.controller.config.imageMaxCount > 0)
144 | Padding(
145 | padding: EdgeInsets.only(right: cameraIconRightPadding),
146 | child: Observer(
147 | builder: (context) => GestureDetector(
148 | onTap: () {
149 | if (store.isSending ||
150 | store.reachImageSelectionLimit) {
151 | return;
152 | }
153 | widget.onCameraTap.call();
154 | },
155 | child: SvgPicture.asset(
156 | 'assets/svg/input/camera.svg',
157 | package: kChatPackage,
158 | width: cameraIconWidth,
159 | height: cameraIconWidth,
160 | colorFilter: ColorFilter.mode(
161 | store.isSending || store.reachImageSelectionLimit
162 | ? Colors.black.withAlpha((0.3 * 255).round())
163 | : Colors.black,
164 | BlendMode.srcIn,
165 | ),
166 | ),
167 | ),
168 | ),
169 | ),
170 | if (widget.controller.config.imageMaxCount > 0)
171 | Padding(
172 | padding: EdgeInsets.only(right: albumIconRightPadding),
173 | child: Observer(
174 | builder: (context) => GestureDetector(
175 | onTap: () {
176 | if (store.isSending ||
177 | store.reachImageSelectionLimit) {
178 | return;
179 | }
180 | widget.onAlbumTap.call();
181 | },
182 | child: SvgPicture.asset(
183 | 'assets/svg/input/album.svg',
184 | package: kChatPackage,
185 | width: albumIconWidth,
186 | height: albumIconWidth,
187 | colorFilter: ColorFilter.mode(
188 | store.isSending || store.reachImageSelectionLimit
189 | ? Colors.black.withAlpha((0.3 * 255).round())
190 | : Colors.black,
191 | BlendMode.srcIn,
192 | ),
193 | ),
194 | ),
195 | ),
196 | ),
197 | Padding(
198 | padding: const EdgeInsets.only(right: sendMsgBtnRightPadding),
199 | child: Observer(
200 | builder: (context) => SendMsgBtn(
201 | size: sendMsgBtnWidth,
202 | isSending: store.isSending,
203 | isDisabled: store.isInputBoxFocused &&
204 | widget.textEditingController.text.isEmpty &&
205 | widget.controller.store.imageFiles.isEmpty,
206 | onTap: () {
207 | if (store.isSending) {
208 | return;
209 | }
210 | if (widget.textEditingController.text.isEmpty &&
211 | widget.controller.store.imageFiles.isEmpty) {
212 | return;
213 | }
214 | widget.onSend.call();
215 | },
216 | ),
217 | ),
218 | ),
219 | ],
220 | ),
221 | );
222 | wrapChildren.add(
223 | Align(
224 | alignment: Alignment.centerRight,
225 | child: buttonBox,
226 | ),
227 | );
228 | return SizedBox(
229 | width: double.infinity,
230 | child: isAloneInRow
231 | ? Column(
232 | crossAxisAlignment: CrossAxisAlignment.stretch,
233 | children: wrapChildren,
234 | )
235 | : Row(
236 | children: wrapChildren,
237 | ),
238 | );
239 | },
240 | ),
241 | );
242 | }
243 |
244 | double _calculateTextFieldWidth(BuildContext context) {
245 | final text = widget.textEditingController.text;
246 | final textPainter = TextPainter(
247 | text: TextSpan(
248 | text: text,
249 | style: DefaultTextStyle.of(context).style.merge(textFieldStyle),
250 | ),
251 | maxLines: 1,
252 | textAlign: TextAlign.left,
253 | textDirection: TextDirection.ltr,
254 | textScaler: MediaQuery.of(context).textScaler,
255 | )..layout(minWidth: 0, maxWidth: double.infinity);
256 |
257 | final hintText = widget.controller.config.inputBoxHintText;
258 | final hintTextPainter = TextPainter(
259 | text: TextSpan(
260 | text: hintText,
261 | style: DefaultTextStyle.of(context).style.merge(textFieldStyle),
262 | ),
263 | maxLines: 1,
264 | textAlign: TextAlign.left,
265 | textDirection: TextDirection.ltr,
266 | textScaler: MediaQuery.of(context).textScaler,
267 | )..layout(minWidth: 0, maxWidth: double.infinity);
268 |
269 | return max(textPainter.size.width, hintTextPainter.size.width) +
270 | textFieldHorizontalPadding * 2 +
271 | cursorWidth;
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/lib/widgets/input/send_msg_btn.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_svg/svg.dart';
3 | import 'package:simple_chat/simple_chat.dart';
4 |
5 | /// The send message button for the chat.
6 | class SendMsgBtn extends StatelessWidget {
7 | /// The is sending flag.
8 | final bool isSending;
9 |
10 | /// The is disabled flag.
11 | final bool isDisabled;
12 |
13 | /// The on tap callback.
14 | final void Function()? onTap;
15 |
16 | /// The size of the button.
17 | final double size;
18 |
19 | /// The constructor for the send message button.
20 | const SendMsgBtn({
21 | super.key,
22 | this.onTap,
23 | this.isDisabled = false,
24 | this.isSending = false,
25 | this.size = 32,
26 | });
27 | @override
28 | Widget build(BuildContext context) {
29 | Widget view;
30 | if (isSending) {
31 | view = Container(
32 | width: size,
33 | height: size,
34 | padding: const EdgeInsets.all(4),
35 | child: CircularProgressIndicator(
36 | color: context.coloredTheme.primary,
37 | ),
38 | );
39 | } else if (isDisabled) {
40 | view = SvgPicture.asset(
41 | 'assets/svg/input/send_disabled.svg',
42 | package: kChatPackage,
43 | width: size,
44 | height: size,
45 | );
46 | } else {
47 | view = SvgPicture.asset(
48 | 'assets/svg/input/send.svg',
49 | package: kChatPackage,
50 | width: size,
51 | height: size,
52 | );
53 | }
54 | return GestureDetector(
55 | onTap: () {
56 | if (isDisabled) return;
57 | onTap?.call();
58 | },
59 | child: view,
60 | );
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib/widgets/messages/image_message_item.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:simple_chat/simple_chat.dart';
5 | import 'package:simple_chat/widgets/messages/message_bubble.dart';
6 |
7 | /// The image message item for the chat.
8 | class ImageMessageItem extends StatelessWidget {
9 | /// The image message.
10 | final ModelImageMessage imageMessage;
11 |
12 | /// The flag for the message from the current user.
13 | final bool isMessageFromCurrentUser;
14 |
15 | /// The constructor for the image message item.
16 | const ImageMessageItem({
17 | super.key,
18 | required this.imageMessage,
19 | required this.isMessageFromCurrentUser,
20 | });
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | final imageUrl = imageMessage.imageUrls.firstOrNull;
25 | Widget? child;
26 | if (imageUrl != null) {
27 | if (imageUrl.startsWith('http')) {
28 | child = Image.network(imageUrl);
29 | } else {
30 | child = Image.file(File(imageUrl));
31 | }
32 | }
33 | return MessageBubble(
34 | padding: EdgeInsets.zero,
35 | isCurrentUser: isMessageFromCurrentUser,
36 | child: child,
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/widgets/messages/loading_indicator_item.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:lottie/lottie.dart';
3 | import 'package:simple_chat/models/loading_indicator_message.dart';
4 | import 'package:simple_chat/simple_chat.dart';
5 | import 'package:simple_chat/widgets/messages/message_bubble.dart';
6 |
7 | /// The loading indicator item for the chat.
8 | class LoadingIndicatorItem extends StatelessWidget {
9 | /// The message.
10 | final ModelLoadingIndicatorMessage message;
11 |
12 | /// The flag for the message from the current user.
13 | final bool isMessageFromCurrentUser;
14 |
15 | /// The constructor for the loading indicator item.
16 | const LoadingIndicatorItem({
17 | super.key,
18 | required this.message,
19 | required this.isMessageFromCurrentUser,
20 | });
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return MessageBubble(
25 | isCurrentUser: isMessageFromCurrentUser,
26 | padding: const EdgeInsets.all(12),
27 | child: SizedBox(
28 | height: 20,
29 | width: 40,
30 | child: Lottie.asset(
31 | 'assets/lottie/speech_loading.json',
32 | repeat: true,
33 | package: kChatPackage,
34 | ),
35 | ),
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/widgets/messages/message_bubble.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:simple_chat/theme/chat_theme.dart';
3 |
4 | /// The message bubble for the chat.
5 | class MessageBubble extends StatelessWidget {
6 | /// The padding of the message bubble.
7 | final EdgeInsets padding;
8 |
9 | /// The flag for the message from the current user.
10 | final bool isCurrentUser;
11 |
12 | /// The child of the message bubble.
13 | final Widget? child;
14 |
15 | /// The constructor for the message bubble.
16 | const MessageBubble({
17 | super.key,
18 | required this.isCurrentUser,
19 | this.padding = const EdgeInsets.all(12),
20 | this.child,
21 | });
22 |
23 | @override
24 | Widget build(BuildContext context) {
25 | return ClipRRect(
26 | borderRadius: BorderRadius.circular(20),
27 | child: Container(
28 | padding: padding,
29 | color: isCurrentUser
30 | ? context.coloredTheme.myMessageColor
31 | : context.coloredTheme.otherMessageColor,
32 | child: child,
33 | ),
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/widgets/messages/text_message_item.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:simple_chat/simple_chat.dart';
3 | import 'package:simple_chat/widgets/messages/message_bubble.dart';
4 |
5 | /// The text message item for the chat.
6 | class TextMessageItem extends StatelessWidget {
7 | /// The text message.
8 | final ModelTextMessage textMessage;
9 |
10 | /// The flag for the message from the current user.
11 | final bool isMessageFromCurrentUser;
12 |
13 | /// The constructor for the text message item.
14 | const TextMessageItem({
15 | super.key,
16 | required this.textMessage,
17 | required this.isMessageFromCurrentUser,
18 | });
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | return MessageBubble(
23 | isCurrentUser: isMessageFromCurrentUser,
24 | child: Text(
25 | textMessage.text,
26 | maxLines: null,
27 | style: const TextStyle(
28 | color: Colors.black,
29 | fontSize: 16,
30 | fontWeight: FontWeight.w400,
31 | height: 1.5,
32 | ),
33 | ),
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/widgets/messages/unsupport_message_item.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:simple_chat/widgets/messages/message_bubble.dart';
3 |
4 | /// The unsupport message item for the chat.
5 | class UnsupportMessageItem extends StatelessWidget {
6 | /// The flag for the message from the current user.
7 | final bool isCurrentUser;
8 |
9 | /// The constructor for the unsupport message item.
10 | const UnsupportMessageItem({super.key, required this.isCurrentUser});
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return MessageBubble(
15 | isCurrentUser: isCurrentUser,
16 | child: const Text(
17 | '[Unsupported message]',
18 | style: TextStyle(
19 | fontSize: 16,
20 | fontWeight: FontWeight.w500,
21 | height: 1.5,
22 | ),
23 | ),
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/widgets/users/user_avatar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_svg/svg.dart';
3 | import 'package:simple_chat/simple_chat.dart';
4 |
5 | /// The user avatar for the chat.
6 | class UserAvatar extends StatelessWidget {
7 | /// The constructor for the user avatar.
8 | const UserAvatar({
9 | super.key,
10 | required this.user,
11 | this.onTap,
12 | });
13 |
14 | /// The user.
15 | final ModelBaseUser? user;
16 |
17 | /// The on tap callback.
18 | final void Function()? onTap;
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | final avatarUrl = user?.avatarUrl;
23 | Widget child;
24 | if (avatarUrl == null) {
25 | child = _EmptyProfileAvatar(
26 | size: context.layoutTheme.userAvatarSize, name: user?.name);
27 | } else if (avatarUrl.contains('http')) {
28 | child = Image.network(
29 | avatarUrl,
30 | width: context.layoutTheme.userAvatarSize,
31 | height: context.layoutTheme.userAvatarSize,
32 | fit: BoxFit.cover,
33 | );
34 | } else if (avatarUrl.contains('svg')) {
35 | child = SvgPicture.asset(
36 | avatarUrl,
37 | width: context.layoutTheme.userAvatarSize,
38 | height: context.layoutTheme.userAvatarSize,
39 | fit: BoxFit.cover,
40 | );
41 | } else {
42 | child = Image.asset(
43 | avatarUrl,
44 | width: context.layoutTheme.userAvatarSize,
45 | height: context.layoutTheme.userAvatarSize,
46 | fit: BoxFit.cover,
47 | );
48 | }
49 |
50 | return GestureDetector(
51 | onTap: onTap,
52 | child: ClipRRect(
53 | borderRadius:
54 | BorderRadius.circular(context.layoutTheme.userAvatarSize / 2),
55 | child: child,
56 | ),
57 | );
58 | }
59 | }
60 |
61 | class _EmptyProfileAvatar extends StatelessWidget {
62 | final double size;
63 | final String? name;
64 | const _EmptyProfileAvatar({
65 | required this.size,
66 | this.name,
67 | });
68 | @override
69 | Widget build(BuildContext context) {
70 | return Container(
71 | width: size,
72 | height: size,
73 | alignment: Alignment.center,
74 | decoration: BoxDecoration(
75 | borderRadius: BorderRadius.circular(size / 2),
76 | color: context.coloredTheme.primary,
77 | ),
78 | child: Text(
79 | name == null || name!.isEmpty ? '' : name!.substring(0, 1),
80 | style: TextStyle(
81 | fontSize: 20 * size / 46,
82 | fontWeight: FontWeight.bold,
83 | ),
84 | ),
85 | );
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: simple_chat
2 | description:
3 | "A simple UI solution for quick integration of IM chat & AI bot chat.
4 | Supports customised Message Cell, message grouping, image preview and more."
5 | version: 0.2.7
6 | repository: https://github.com/Tealseed-Lab/simple_chat
7 | issue_tracker: https://github.com/Tealseed-Lab/simple_chat/issues
8 |
9 | environment:
10 | sdk: ">=3.4.3 <4.0.0"
11 | flutter: ">=1.17.0"
12 |
13 | dependencies:
14 | flutter:
15 | sdk: flutter
16 | mobx: ^2.3.3+2
17 | flutter_mobx: ^2.2.1+1
18 | json_annotation: ^4.9.0
19 | flutter_svg: ^2.0.7
20 | logger: ^2.4.0
21 | lottie: ^3.1.2
22 | uuid: ^4.5.0
23 | visibility_detector: ^0.4.0+2
24 | easy_asset_picker: ^0.0.5
25 | image_picker: ^1.1.2
26 |
27 | dev_dependencies:
28 | build_runner: ^2.4.10
29 | flutter_lints: ^5.0.0
30 | mobx_codegen: ^2.6.1
31 | json_serializable: ^6.8.0
32 |
33 | flutter:
34 | assets:
35 | - assets/svg/input/
36 | - assets/svg/chat/
37 | - assets/lottie/
38 |
--------------------------------------------------------------------------------