├── .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 | [![Star History Chart](https://api.star-history.com/svg?repos=Tealseed-Lab/simple_chat&type=Date)](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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/svg/input/album.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/svg/input/camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/svg/input/send.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/svg/input/send_disabled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | --------------------------------------------------------------------------------