├── doc ├── entryPoint.md ├── customization.md ├── models.md └── defaultWidgets.md ├── DashChat.png ├── example ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── 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-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── .gitignore │ ├── Podfile │ └── Podfile.lock ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ └── Icon-512.png │ ├── manifest.json │ └── index.html ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── 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 │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── examples │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── .metadata ├── README.md ├── lib │ ├── samples │ │ ├── media.dart │ │ ├── avatar.dart │ │ ├── typing_users_sample.dart │ │ ├── basic.dart │ │ ├── send_on_enter.dart │ │ ├── quick_replies_sample.dart │ │ ├── theming.dart │ │ └── mention.dart │ ├── main.dart │ └── data.dart ├── .gitignore ├── analysis_options.yaml └── pubspec.yaml ├── assets ├── placeholder.png └── profile_placeholder.png ├── test └── dash_chat_test.dart ├── lib ├── src │ ├── widgets │ │ ├── image_provider │ │ │ ├── image_provider.dart │ │ │ ├── image_provider_web.dart │ │ │ └── image_provider_mobile.dart │ │ ├── input_toolbar │ │ │ ├── default_send_button.dart │ │ │ ├── default_input_decoration.dart │ │ │ └── input_toolbar.dart │ │ ├── message_row │ │ │ ├── default_message_decoration.dart │ │ │ ├── default_user_name.dart │ │ │ ├── default_parse_patterns.dart │ │ │ ├── video_player.dart │ │ │ ├── text_container.dart │ │ │ ├── default_avatar.dart │ │ │ ├── default_message_text.dart │ │ │ ├── media_container.dart │ │ │ └── message_row.dart │ │ ├── typing_users │ │ │ ├── default_typing_builder.dart │ │ │ └── typing_indicator.dart │ │ ├── quick_replies │ │ │ ├── quick_replies.dart │ │ │ └── default_quick_reply.dart │ │ └── message_list │ │ │ ├── default_date_separator.dart │ │ │ ├── default_scroll_to_bottom.dart │ │ │ └── message_list.dart │ ├── helpers │ │ └── link_helper.dart │ ├── models │ │ ├── cursor_style.dart │ │ ├── scroll_to_bottom_options.dart │ │ ├── mention.dart │ │ ├── quick_reply_options.dart │ │ ├── quick_reply.dart │ │ ├── chat_user.dart │ │ ├── message_list_options.dart │ │ ├── chat_media.dart │ │ ├── input_options.dart │ │ ├── chat_message.dart │ │ └── message_options.dart │ └── dash_chat.dart └── dash_chat_2.dart ├── .metadata ├── dartdoc_options.yaml ├── LICENSE ├── .gitignore ├── pubspec.yaml ├── CHANGELOG.md ├── analysis_options.yaml ├── .all-contributorsrc ├── README.md └── pubspec.lock /doc/entryPoint.md: -------------------------------------------------------------------------------- 1 | # Entry Point 2 | 3 | The overall widget of the lib -------------------------------------------------------------------------------- /DashChat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/DashChat.png -------------------------------------------------------------------------------- /doc/customization.md: -------------------------------------------------------------------------------- 1 | # Customizations 2 | 3 | Options to customize the UI of the chat -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /doc/models.md: -------------------------------------------------------------------------------- 1 | # Models 2 | 3 | Data structures to pass your messages, users... to the lib -------------------------------------------------------------------------------- /assets/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/assets/placeholder.png -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/example/web/favicon.png -------------------------------------------------------------------------------- /assets/profile_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/assets/profile_placeholder.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /test/dash_chat_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | void main() { 4 | test('', () {}); 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/widgets/image_provider/image_provider.dart: -------------------------------------------------------------------------------- 1 | export 'image_provider_mobile.dart' 2 | if (dart.library.html) 'image_provider_web.dart'; 3 | -------------------------------------------------------------------------------- /doc/defaultWidgets.md: -------------------------------------------------------------------------------- 1 | # Default widgets 2 | 3 | Widgets you can use if you only want to override few params but don't want to rewrite the entire widget -------------------------------------------------------------------------------- /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/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /lib/src/helpers/link_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:url_launcher/url_launcher.dart'; 2 | 3 | void openLink(String link) { 4 | launchUrl(Uri.parse(link), mode: LaunchMode.externalApplication); 5 | } 6 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/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/SebastienBtr/Dash-Chat-2/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SebastienBtr/Dash-Chat-2/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/examples/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.examples 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.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: f4abaa0735eba4dfd8f33f73363911d63931fe03 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /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: f4abaa0735eba4dfd8f33f73363911d63931fe03 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /lib/src/widgets/image_provider/image_provider_web.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | ImageProvider getImageProvider(String url) { 4 | if (url.startsWith('http')) { 5 | return NetworkImage(url); 6 | } else { 7 | return const AssetImage( 8 | 'assets/placeholder.png', 9 | package: 'dash_chat_2', 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /dartdoc_options.yaml: -------------------------------------------------------------------------------- 1 | dartdoc: 2 | categories: 3 | "Entry point": 4 | markdown: doc/entryPoint.md 5 | "Models": 6 | markdown: doc/models.md 7 | "Customization": 8 | markdown: doc/customization.md 9 | "Default widgets": 10 | markdown: doc/defaultWidgets.md 11 | categoryOrder: ["Entry point", "Models", "Customization", "Default widgets"] 12 | showUndocumentedCategories: true 13 | -------------------------------------------------------------------------------- /lib/src/models/cursor_style.dart: -------------------------------------------------------------------------------- 1 | part of '../../dash_chat_2.dart'; 2 | 3 | /// {@category Customization} 4 | class CursorStyle { 5 | const CursorStyle({ 6 | this.color, 7 | this.hide = false, 8 | this.width = 2.0, 9 | }); 10 | 11 | /// Color of the cursor 12 | final Color? color; 13 | 14 | /// Hide or not the cursor 15 | final bool hide; 16 | 17 | /// Width of the cursor 18 | final double width; 19 | } 20 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/widgets/image_provider/image_provider_mobile.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:cached_network_image/cached_network_image.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | ImageProvider getImageProvider(String url) { 7 | if (url.startsWith('http')) { 8 | return CachedNetworkImageProvider(url); 9 | } else if (url.startsWith('assets')) { 10 | return AssetImage(url); 11 | } else { 12 | return FileImage( 13 | File(url), 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /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/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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/README.md: -------------------------------------------------------------------------------- 1 | # examples 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://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /lib/src/widgets/input_toolbar/default_send_button.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// {@category Default widgets} 4 | Widget Function(Function send) defaultSendButton({ 5 | required Color color, 6 | IconData icon = Icons.send, 7 | EdgeInsets? padding, 8 | bool disabled = false, 9 | }) => 10 | (Function fct) => InkWell( 11 | onTap: disabled ? null : () => fct(), 12 | child: Padding( 13 | padding: padding ?? 14 | const EdgeInsets.symmetric(horizontal: 8, vertical: 10), 15 | child: Icon( 16 | icon, 17 | color: color, 18 | ), 19 | ), 20 | ); 21 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "short_name": "examples", 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 | } 24 | -------------------------------------------------------------------------------- /lib/src/widgets/message_row/default_message_decoration.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// {@category Default widgets} 4 | BoxDecoration defaultMessageDecoration({ 5 | required Color color, 6 | required double borderTopLeft, 7 | required double borderTopRight, 8 | required double borderBottomLeft, 9 | required double borderBottomRight, 10 | }) => 11 | BoxDecoration( 12 | color: color, 13 | borderRadius: BorderRadius.only( 14 | topLeft: Radius.circular(borderTopLeft), 15 | topRight: Radius.circular(borderTopRight), 16 | bottomLeft: Radius.circular(borderBottomLeft), 17 | bottomRight: Radius.circular(borderBottomRight), 18 | ), 19 | ); 20 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.9.0' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.0.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | tasks.register("clean", Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/ephemeral/ 22 | Flutter/app.flx 23 | Flutter/app.zip 24 | Flutter/flutter_assets/ 25 | Flutter/flutter_export_environment.sh 26 | ServiceDefinitions.json 27 | Runner/GeneratedPluginRegistrant.* 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /lib/src/models/scroll_to_bottom_options.dart: -------------------------------------------------------------------------------- 1 | part of '../../dash_chat_2.dart'; 2 | 3 | /// {@category Customization} 4 | class ScrollToBottomOptions { 5 | const ScrollToBottomOptions({ 6 | this.disabled = false, 7 | this.scrollToBottomBuilder, 8 | this.onScrollToBottomPress, 9 | }); 10 | 11 | /// If you don't want to show the scroll-to-bottom widget 12 | final bool disabled; 13 | 14 | /// Builder to create your own scroll-to-bottom widget 15 | /// You can use DefaultScrollToBottom to only override some variables 16 | final Widget Function(ScrollController scrollController)? 17 | scrollToBottomBuilder; 18 | 19 | /// Function to call when the scroll-to-bottom widget is pressed 20 | /// It will scroll down in any case 21 | final void Function()? onScrollToBottomPress; 22 | } 23 | -------------------------------------------------------------------------------- /example/lib/samples/media.dart: -------------------------------------------------------------------------------- 1 | import 'package:dash_chat_2/dash_chat_2.dart'; 2 | import 'package:examples/data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class Media extends StatefulWidget { 6 | @override 7 | State createState() => MediaState(); 8 | } 9 | 10 | class MediaState extends State { 11 | List messages = media; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text('Media example'), 18 | ), 19 | body: DashChat( 20 | currentUser: user, 21 | onSend: (ChatMessage m) { 22 | setState(() { 23 | messages.insert(0, m); 24 | }); 25 | }, 26 | messages: messages, 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/lib/samples/avatar.dart: -------------------------------------------------------------------------------- 1 | import 'package:dash_chat_2/dash_chat_2.dart'; 2 | import 'package:examples/data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class AvatarSample extends StatefulWidget { 6 | @override 7 | State createState() => AvatarSampleState(); 8 | } 9 | 10 | class AvatarSampleState extends State { 11 | List messages = allUsersSample; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text('Users example'), 18 | ), 19 | body: DashChat( 20 | currentUser: user, 21 | onSend: (ChatMessage m) { 22 | setState(() { 23 | messages.insert(0, m); 24 | }); 25 | }, 26 | messages: messages, 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/widgets/input_toolbar/default_input_decoration.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// {@category Default widgets} 4 | InputDecoration defaultInputDecoration({ 5 | String hintText = 'Write a message...', 6 | TextStyle hintStyle = const TextStyle(color: Colors.grey), 7 | Color? fillColor, 8 | }) => 9 | InputDecoration( 10 | isDense: true, 11 | hintText: hintText, 12 | hintStyle: hintStyle, 13 | filled: true, 14 | fillColor: fillColor ?? Colors.grey[100], 15 | contentPadding: const EdgeInsets.only( 16 | left: 18, 17 | top: 10, 18 | bottom: 10, 19 | ), 20 | border: OutlineInputBorder( 21 | borderRadius: BorderRadius.circular(25), 22 | borderSide: const BorderSide( 23 | width: 0, 24 | style: BorderStyle.none, 25 | ), 26 | ), 27 | ); 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/src/widgets/message_row/default_user_name.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// {@category Default widgets} 4 | class DefaultUserName extends StatelessWidget { 5 | const DefaultUserName({ 6 | required this.user, 7 | this.style, 8 | this.padding, 9 | Key? key, 10 | }) : super(key: key); 11 | 12 | /// User to show 13 | final ChatUser user; 14 | 15 | /// Style of the text 16 | final TextStyle? style; 17 | 18 | /// Padding around the text 19 | final EdgeInsets? padding; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Padding( 24 | padding: padding ?? const EdgeInsets.symmetric(horizontal: 10), 25 | child: Text( 26 | user.getFullName(), 27 | style: style ?? 28 | const TextStyle( 29 | fontSize: 10, 30 | color: Colors.grey, 31 | ), 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/lib/samples/typing_users_sample.dart: -------------------------------------------------------------------------------- 1 | import 'package:dash_chat_2/dash_chat_2.dart'; 2 | import 'package:examples/data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class TypingUsersSample extends StatefulWidget { 6 | @override 7 | State createState() => TypingUsersSampleState(); 8 | } 9 | 10 | class TypingUsersSampleState extends State { 11 | List messages = basicSample; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text('Typing user example'), 18 | ), 19 | body: DashChat( 20 | currentUser: user, 21 | onSend: (ChatMessage m) { 22 | setState(() { 23 | messages.insert(0, m); 24 | }); 25 | }, 26 | typingUsers: [user3], 27 | messages: messages, 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /lib/src/models/mention.dart: -------------------------------------------------------------------------------- 1 | part of '../../dash_chat_2.dart'; 2 | 3 | /// {@category Models} 4 | class Mention { 5 | Mention({ 6 | required this.title, 7 | this.customProperties, 8 | }); 9 | 10 | /// Create a Mention instance from json data 11 | factory Mention.fromJson(Map jsonData) { 12 | return Mention( 13 | title: jsonData['title'].toString(), 14 | customProperties: jsonData['customProperties'] as Map?, 15 | ); 16 | } 17 | 18 | /// Title of the mention, 19 | /// it's what is visible in the message: @userName 20 | String title; 21 | 22 | /// A list of custom properties to save any data you might need 23 | /// For instance a user Id 24 | Map? customProperties; 25 | 26 | /// Convert a Mention into a json 27 | Map toJson() { 28 | return { 29 | 'title': title, 30 | 'customProperties': customProperties, 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/lib/samples/basic.dart: -------------------------------------------------------------------------------- 1 | import 'package:dash_chat_2/dash_chat_2.dart'; 2 | import 'package:examples/data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class Basic extends StatefulWidget { 6 | @override 7 | State createState() => BasicState(); 8 | } 9 | 10 | class BasicState extends State { 11 | List messages = basicSample; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text('Basic example'), 18 | ), 19 | body: DashChat( 20 | currentUser: user, 21 | onSend: (ChatMessage m) { 22 | setState(() { 23 | messages.insert(0, m); 24 | }); 25 | }, 26 | messages: messages, 27 | messageListOptions: MessageListOptions( 28 | onLoadEarlier: () async { 29 | await Future.delayed(const Duration(seconds: 3)); 30 | }, 31 | ), 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/models/quick_reply_options.dart: -------------------------------------------------------------------------------- 1 | part of '../../dash_chat_2.dart'; 2 | 3 | /// {@category Customization} 4 | class QuickReplyOptions { 5 | const QuickReplyOptions({ 6 | this.onTapQuickReply, 7 | this.quickReplyPadding, 8 | this.quickReplyMargin, 9 | this.quickReplyStyle, 10 | this.quickReplyTextStyle, 11 | this.quickReplyBuilder, 12 | }); 13 | 14 | /// Function to call when the user click on a quick reply 15 | /// Use that to create a message and send it 16 | final Function(QuickReply)? onTapQuickReply; 17 | 18 | /// Padding of a quick reply container 19 | final EdgeInsets? quickReplyPadding; 20 | 21 | /// Margin of a quick reply container 22 | final EdgeInsets? quickReplyMargin; 23 | 24 | /// BoxDecoration of a quick reply container 25 | final BoxDecoration? quickReplyStyle; 26 | 27 | /// TextStyle of a quick reply 28 | final TextStyle? quickReplyTextStyle; 29 | 30 | /// Builder to create your own quickReply builder 31 | final Widget Function(QuickReply)? quickReplyBuilder; 32 | } 33 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Molteo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/lib/samples/send_on_enter.dart: -------------------------------------------------------------------------------- 1 | import 'package:dash_chat_2/dash_chat_2.dart'; 2 | import 'package:examples/data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class SendOnEnter extends StatefulWidget { 6 | @override 7 | State createState() => SendOnEnterState(); 8 | } 9 | 10 | class SendOnEnterState extends State { 11 | List messages = basicSample; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text('SendOnEnter example'), 18 | ), 19 | body: DashChat( 20 | currentUser: user, 21 | onSend: (ChatMessage m) { 22 | setState(() { 23 | messages.insert(0, m); 24 | }); 25 | }, 26 | messages: messages, 27 | inputOptions: const InputOptions( 28 | sendOnEnter: true, 29 | ), 30 | messageListOptions: MessageListOptions( 31 | onLoadEarlier: () async { 32 | await Future.delayed(const Duration(seconds: 3)); 33 | }, 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/widgets/message_row/default_parse_patterns.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | @Deprecated('Use defaultParsePatterns instead') 4 | List defaultPersePatterns = defaultParsePatterns; 5 | 6 | /// {@category Default widgets} 7 | List defaultParsePatterns = [ 8 | MatchText( 9 | type: ParsedType.EMAIL, 10 | style: const TextStyle( 11 | decoration: TextDecoration.underline, 12 | ), 13 | onTap: (String email) { 14 | String url = 'mailto:$email'; 15 | openLink(url); 16 | }, 17 | ), 18 | MatchText( 19 | type: ParsedType.URL, 20 | style: const TextStyle( 21 | decoration: TextDecoration.underline, 22 | ), 23 | onTap: (String url) { 24 | if (!url.startsWith('http://') && !url.startsWith('https://')) { 25 | url = 'http://$url'; 26 | } 27 | openLink(url); 28 | }, 29 | ), 30 | MatchText( 31 | pattern: r'(\+|00|0)?[1-9][0-9 \-\(\)\.]{5,32}[0-9](?!\w)', 32 | type: ParsedType.CUSTOM, 33 | style: const TextStyle( 34 | decoration: TextDecoration.underline, 35 | ), 36 | onTap: (String phone) { 37 | String url = 'tel:$phone'; 38 | openLink(url); 39 | }, 40 | ), 41 | ]; 42 | -------------------------------------------------------------------------------- /lib/src/widgets/typing_users/default_typing_builder.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// {@category Default widgets} 4 | class DefaultTypingBuilder extends StatelessWidget { 5 | const DefaultTypingBuilder({ 6 | required this.user, 7 | this.text = 'is typing', 8 | Key? key, 9 | }) : super(key: key); 10 | 11 | /// User that is typing 12 | final ChatUser user; 13 | 14 | /// Text to show after user's name in the indicator 15 | final String text; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Padding( 20 | padding: const EdgeInsets.only(left: 15, top: 25), 21 | child: Row( 22 | crossAxisAlignment: CrossAxisAlignment.end, 23 | children: [ 24 | const Padding( 25 | padding: EdgeInsets.only(right: 2), 26 | child: TypingIndicator(), 27 | ), 28 | Text( 29 | user.getFullName(), 30 | style: const TextStyle( 31 | fontSize: 12, 32 | fontWeight: FontWeight.bold, 33 | ), 34 | ), 35 | Text( 36 | ' $text', 37 | style: const TextStyle(fontSize: 12), 38 | ), 39 | ], 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/lib/samples/quick_replies_sample.dart: -------------------------------------------------------------------------------- 1 | import 'package:dash_chat_2/dash_chat_2.dart'; 2 | import 'package:examples/data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class QuickRepliesSample extends StatefulWidget { 6 | @override 7 | State createState() => QuickRepliesSampleState(); 8 | } 9 | 10 | class QuickRepliesSampleState extends State { 11 | List messages = quickReplies; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text('QuickReplies example'), 18 | ), 19 | body: DashChat( 20 | currentUser: user, 21 | onSend: (ChatMessage m) { 22 | setState(() { 23 | messages.insert(0, m); 24 | }); 25 | }, 26 | quickReplyOptions: QuickReplyOptions(onTapQuickReply: (QuickReply r) { 27 | final ChatMessage m = ChatMessage( 28 | user: user, 29 | text: r.value ?? r.title, 30 | createdAt: DateTime.now(), 31 | ); 32 | setState(() { 33 | messages.insert(0, m); 34 | }); 35 | }), 36 | messages: messages, 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/models/quick_reply.dart: -------------------------------------------------------------------------------- 1 | part of '../../dash_chat_2.dart'; 2 | 3 | /// {@category Models} 4 | class QuickReply { 5 | QuickReply({ 6 | required this.title, 7 | this.value, 8 | this.customProperties, 9 | }); 10 | 11 | /// Create a QuickReply instance from json data 12 | factory QuickReply.fromJson(Map jsonData) { 13 | return QuickReply( 14 | title: jsonData['title'].toString(), 15 | value: jsonData['value']?.toString(), 16 | customProperties: jsonData['customProperties'] as Map?, 17 | ); 18 | } 19 | 20 | /// Title of the quick reply, 21 | /// it's what will be visible in the quick replies list 22 | String title; 23 | 24 | /// Actual value of the quick reply 25 | /// Use that if you want to have a message text different from the title 26 | String? value; 27 | 28 | /// A list of custom properties to extend the existing ones 29 | /// in case you need to store more things. 30 | /// Can be useful to extend existing features 31 | Map? customProperties; 32 | 33 | /// Convert a QuickReply into a json 34 | Map toJson() { 35 | return { 36 | 'title': title, 37 | 'value': value, 38 | 'customProperties': customProperties, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/widgets/quick_replies/quick_replies.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// @nodoc 4 | class QuickReplies extends StatelessWidget { 5 | const QuickReplies({ 6 | required this.quickReplies, 7 | this.quickReplyOptions = const QuickReplyOptions(), 8 | Key? key, 9 | }) : super(key: key); 10 | 11 | /// List of quick replies to show 12 | final List quickReplies; 13 | 14 | /// Options used to customize quick replies behaviour and design 15 | final QuickReplyOptions quickReplyOptions; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Column( 20 | mainAxisSize: MainAxisSize.min, 21 | crossAxisAlignment: CrossAxisAlignment.start, 22 | children: [ 23 | SingleChildScrollView( 24 | scrollDirection: Axis.horizontal, 25 | child: Padding( 26 | padding: const EdgeInsets.all(8.0), 27 | child: Row( 28 | children: quickReplies.map((QuickReply r) { 29 | return quickReplyOptions.quickReplyBuilder != null 30 | ? quickReplyOptions.quickReplyBuilder!(r) 31 | : DefaultQuickReply( 32 | quickReply: r, 33 | quickReplyOptions: quickReplyOptions, 34 | ); 35 | }).toList(), 36 | ), 37 | ), 38 | ), 39 | ], 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /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 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | use_key_in_widget_constructors: false 26 | prefer_single_quotes: true 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /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 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - path_provider_foundation (0.0.1): 4 | - Flutter 5 | - FlutterMacOS 6 | - sqflite (0.0.3): 7 | - Flutter 8 | - FlutterMacOS 9 | - url_launcher_ios (0.0.1): 10 | - Flutter 11 | - video_player_avfoundation (0.0.1): 12 | - Flutter 13 | - FlutterMacOS 14 | 15 | DEPENDENCIES: 16 | - Flutter (from `Flutter`) 17 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 18 | - sqflite (from `.symlinks/plugins/sqflite/darwin`) 19 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 20 | - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) 21 | 22 | EXTERNAL SOURCES: 23 | Flutter: 24 | :path: Flutter 25 | path_provider_foundation: 26 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 27 | sqflite: 28 | :path: ".symlinks/plugins/sqflite/darwin" 29 | url_launcher_ios: 30 | :path: ".symlinks/plugins/url_launcher_ios/ios" 31 | video_player_avfoundation: 32 | :path: ".symlinks/plugins/video_player_avfoundation/darwin" 33 | 34 | SPEC CHECKSUMS: 35 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 36 | path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 37 | sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec 38 | url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe 39 | video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 40 | 41 | PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 42 | 43 | COCOAPODS: 1.13.0 44 | -------------------------------------------------------------------------------- /lib/src/widgets/quick_replies/default_quick_reply.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// {@category Default widgets} 4 | class DefaultQuickReply extends StatelessWidget { 5 | const DefaultQuickReply({ 6 | required this.quickReply, 7 | this.quickReplyOptions = const QuickReplyOptions(), 8 | Key? key, 9 | }) : super(key: key); 10 | 11 | /// Options used to customize quick replies behaviour and design 12 | final QuickReplyOptions quickReplyOptions; 13 | 14 | /// Quick reply to show 15 | final QuickReply quickReply; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return GestureDetector( 20 | onTap: quickReplyOptions.onTapQuickReply != null 21 | ? () => quickReplyOptions.onTapQuickReply!(quickReply) 22 | : null, 23 | child: Container( 24 | margin: quickReplyOptions.quickReplyMargin ?? 25 | const EdgeInsets.symmetric(horizontal: 5), 26 | padding: quickReplyOptions.quickReplyPadding ?? 27 | const EdgeInsets.symmetric(horizontal: 8, vertical: 5), 28 | decoration: quickReplyOptions.quickReplyStyle ?? 29 | BoxDecoration( 30 | border: Border.all(color: Theme.of(context).primaryColor), 31 | borderRadius: const BorderRadius.all(Radius.circular(5.0)), 32 | ), 33 | child: Text( 34 | quickReply.title, 35 | style: quickReplyOptions.quickReplyTextStyle ?? 36 | TextStyle(color: Theme.of(context).primaryColor), 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/ephemeral 64 | **/ios/Flutter/app.flx 65 | **/ios/Flutter/app.zip 66 | **/ios/Flutter/flutter_assets/ 67 | **/ios/Flutter/flutter_export_environment.sh 68 | **/ios/ServiceDefinitions.json 69 | **/ios/Runner/GeneratedPluginRegistrant.* 70 | 71 | # Exceptions to above rules. 72 | !**/ios/**/default.mode1v3 73 | !**/ios/**/default.mode2v3 74 | !**/ios/**/default.pbxuser 75 | !**/ios/**/default.perspectivev3 76 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | examples 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /lib/src/models/chat_user.dart: -------------------------------------------------------------------------------- 1 | part of '../../dash_chat_2.dart'; 2 | 3 | /// {@category Models} 4 | class ChatUser { 5 | ChatUser({ 6 | required this.id, 7 | this.profileImage, 8 | this.customProperties, 9 | this.firstName, 10 | this.lastName, 11 | }); 12 | 13 | /// Create a ChatUser instance from json data 14 | factory ChatUser.fromJson(Map jsonData) { 15 | return ChatUser( 16 | id: jsonData['id'].toString(), 17 | profileImage: jsonData['profileImage']?.toString(), 18 | firstName: jsonData['firstName']?.toString(), 19 | lastName: jsonData['lastName']?.toString(), 20 | customProperties: jsonData['customProperties'] as Map?, 21 | ); 22 | } 23 | 24 | /// Id of the user 25 | String id; 26 | 27 | /// Profile image of the user 28 | String? profileImage; 29 | 30 | /// A list of custom properties to extend the existing ones 31 | /// in case you need to store more things. 32 | /// Can be useful to extend existing features 33 | Map? customProperties; 34 | 35 | /// First name of the user, 36 | /// if you only have the name as one string 37 | /// you can put the entire value in the [firstName] field 38 | String? firstName; 39 | 40 | /// Last name of the user 41 | String? lastName; 42 | 43 | /// Get the full name (firstName + lastName) of the user 44 | String getFullName() { 45 | return (firstName ?? '') + 46 | (firstName != null && lastName != null 47 | ? ' ${lastName!}' 48 | : lastName ?? ''); 49 | } 50 | 51 | /// Convert a ChatUser into a json 52 | Map toJson() { 53 | return { 54 | 'id': id, 55 | 'profileImage': profileImage, 56 | 'firstName': firstName, 57 | 'lastName': lastName, 58 | 'customProperties': customProperties, 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/widgets/message_list/default_date_separator.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// {@category Default widgets} 4 | class DefaultDateSeparator extends StatelessWidget { 5 | const DefaultDateSeparator({ 6 | required this.date, 7 | this.messageListOptions = const MessageListOptions(), 8 | this.padding = const EdgeInsets.symmetric(vertical: 20), 9 | this.textStyle = const TextStyle(color: Colors.grey), 10 | Key? key, 11 | }) : super(key: key); 12 | 13 | /// Date to show 14 | final DateTime date; 15 | 16 | /// Options to customize the behaviour and design of the overall list of message 17 | final MessageListOptions messageListOptions; 18 | 19 | /// Padding of the separator 20 | final EdgeInsets padding; 21 | 22 | /// Style of the text 23 | final TextStyle textStyle; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Padding( 28 | padding: padding, 29 | child: Text( 30 | _formatDateSeparator(date), 31 | style: textStyle, 32 | ), 33 | ); 34 | } 35 | 36 | String _formatDateSeparator(DateTime date) { 37 | if (messageListOptions.dateSeparatorFormat != null) { 38 | return messageListOptions.dateSeparatorFormat!.format(date); 39 | } 40 | 41 | final DateTime today = DateTime.now(); 42 | 43 | if (date.year != today.year) { 44 | return intl.DateFormat('dd MMM yyyy, HH:mm').format(date); 45 | } else if (date.month != today.month || 46 | _getWeekOfYear(date) != _getWeekOfYear(today)) { 47 | return intl.DateFormat('dd MMM HH:mm').format(date); 48 | } else if (date.day != today.day) { 49 | return intl.DateFormat('E HH:mm').format(date); 50 | } 51 | return intl.DateFormat('HH:mm').format(date); 52 | } 53 | 54 | int _getWeekOfYear(DateTime date) { 55 | final int dayOfYear = int.parse(intl.DateFormat('D').format(date)); 56 | return ((dayOfYear - date.weekday + 10) / 7).floor(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dash_chat_2 2 | description: The most complete Chat UI for flutter. Easy to use, highly customizable and fully featured 3 | version: 0.0.21 4 | homepage: https://github.com/SebastienBtr/Dash-Chat-2 5 | 6 | environment: 7 | sdk: ">=2.12.0 <4.0.0" 8 | flutter: ">=1.17.0" 9 | 10 | dependencies: 11 | cached_network_image: ^3.3.1 12 | flutter: 13 | sdk: flutter 14 | flutter_markdown: ^0.7.1 15 | flutter_parsed_text: ^2.2.1 16 | intl: ^0.19.0 17 | url_launcher: ^6.0.10 18 | video_player: ^2.2.5 19 | 20 | dev_dependencies: 21 | flutter_lints: ^3.0.1 22 | flutter_test: 23 | sdk: flutter 24 | 25 | # For information on the generic Dart part of this file, see the 26 | # following page: https://dart.dev/tools/pub/pubspec 27 | 28 | # The following section is specific to Flutter. 29 | flutter: 30 | 31 | # To add assets to your package, add an assets section, like this: 32 | assets: 33 | - assets/ 34 | # 35 | # For details regarding assets in packages, see 36 | # https://flutter.dev/assets-and-images/#from-packages 37 | # 38 | # An image asset can refer to one or more resolution-specific "variants", see 39 | # https://flutter.dev/assets-and-images/#resolution-aware. 40 | 41 | # To add custom fonts to your package, add a fonts section here, 42 | # in this "flutter" section. Each entry in this list should have a 43 | # "family" key with the font family name, and a "fonts" key with a 44 | # list giving the asset and other descriptors for the font. For 45 | # example: 46 | # fonts: 47 | # - family: Schyler 48 | # fonts: 49 | # - asset: fonts/Schyler-Regular.ttf 50 | # - asset: fonts/Schyler-Italic.ttf 51 | # style: italic 52 | # - family: Trajan Pro 53 | # fonts: 54 | # - asset: fonts/TrajanPro.ttf 55 | # - asset: fonts/TrajanPro_Bold.ttf 56 | # weight: 700 57 | # 58 | # For details regarding fonts in packages, see 59 | # https://flutter.dev/custom-fonts/#from-packages 60 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 33 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "com.example.examples" 38 | minSdkVersion flutter.minSdkVersion 39 | targetSdkVersion 30 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | multiDexEnabled true 43 | } 44 | 45 | buildTypes { 46 | release { 47 | // TODO: Add your own signing config for the release build. 48 | // Signing with the debug keys for now, so `flutter run --release` works. 49 | signingConfig signingConfigs.debug 50 | } 51 | } 52 | } 53 | 54 | flutter { 55 | source '../..' 56 | } 57 | 58 | dependencies { 59 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 60 | implementation 'androidx.multidex:multidex:2.0.1' 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/models/message_list_options.dart: -------------------------------------------------------------------------------- 1 | part of '../../dash_chat_2.dart'; 2 | 3 | /// {@category Customization} 4 | class MessageListOptions { 5 | const MessageListOptions({ 6 | this.showDateSeparator = true, 7 | this.dateSeparatorFormat, 8 | this.dateSeparatorBuilder, 9 | this.separatorFrequency = SeparatorFrequency.days, 10 | this.scrollController, 11 | this.chatFooterBuilder, 12 | this.showFooterBeforeQuickReplies = false, 13 | this.loadEarlierBuilder, 14 | this.onLoadEarlier, 15 | this.typingBuilder, 16 | this.scrollPhysics, 17 | }); 18 | 19 | /// If you want to who a date separator between messages of different dates 20 | final bool showDateSeparator; 21 | 22 | /// The formatting of the date in the date separator. 23 | /// By default it will adapt according to the difference with today 24 | final intl.DateFormat? dateSeparatorFormat; 25 | 26 | /// If you want to create you own separator widget 27 | /// You can use DefaultDateSeparator to only override some variables 28 | final Widget Function(DateTime date)? dateSeparatorBuilder; 29 | 30 | /// The frequency of the separator 31 | final SeparatorFrequency separatorFrequency; 32 | 33 | /// Scroll controller of the list of message 34 | final ScrollController? scrollController; 35 | 36 | /// A widget to show at the bottom of the chat 37 | /// (between the input and the chat content) 38 | final Widget? chatFooterBuilder; 39 | 40 | /// If you want to show [chatFooterBuilder] before or after the quick replies 41 | final bool showFooterBeforeQuickReplies; 42 | 43 | /// If you want to show a widget when the top of the list is reached 44 | final Widget? loadEarlierBuilder; 45 | 46 | /// Function to call when the top of the list is reached 47 | /// Useful to load more messages 48 | final Future Function()? onLoadEarlier; 49 | 50 | /// Builder to create your own typing widget 51 | final Widget Function(ChatUser user)? typingBuilder; 52 | 53 | /// Scroll physics of the ListView 54 | final ScrollPhysics? scrollPhysics; 55 | } 56 | 57 | enum SeparatorFrequency { days, hours } 58 | -------------------------------------------------------------------------------- /lib/dash_chat_2.dart: -------------------------------------------------------------------------------- 1 | library dash_chat_2; 2 | 3 | import 'dart:math'; 4 | 5 | import 'package:flutter/gestures.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_markdown/flutter_markdown.dart'; 8 | import 'package:flutter_parsed_text/flutter_parsed_text.dart'; 9 | import 'package:intl/intl.dart' as intl; 10 | import 'package:video_player/video_player.dart' as vp; 11 | 12 | import 'src/helpers/link_helper.dart'; 13 | import 'src/widgets/image_provider/image_provider.dart'; 14 | 15 | export 'package:flutter_parsed_text/flutter_parsed_text.dart'; 16 | 17 | part 'src/dash_chat.dart'; 18 | part 'src/models/chat_media.dart'; 19 | part 'src/models/chat_message.dart'; 20 | part 'src/models/chat_user.dart'; 21 | part 'src/models/cursor_style.dart'; 22 | part 'src/models/input_options.dart'; 23 | part 'src/models/mention.dart'; 24 | part 'src/models/message_list_options.dart'; 25 | part 'src/models/message_options.dart'; 26 | part 'src/models/quick_reply.dart'; 27 | part 'src/models/quick_reply_options.dart'; 28 | part 'src/models/scroll_to_bottom_options.dart'; 29 | part 'src/widgets/input_toolbar/default_input_decoration.dart'; 30 | part 'src/widgets/input_toolbar/default_send_button.dart'; 31 | part 'src/widgets/input_toolbar/input_toolbar.dart'; 32 | part 'src/widgets/message_list/default_date_separator.dart'; 33 | part 'src/widgets/message_list/default_scroll_to_bottom.dart'; 34 | part 'src/widgets/message_list/message_list.dart'; 35 | part 'src/widgets/message_row/default_avatar.dart'; 36 | part 'src/widgets/message_row/default_message_decoration.dart'; 37 | part 'src/widgets/message_row/default_message_text.dart'; 38 | part 'src/widgets/message_row/default_parse_patterns.dart'; 39 | part 'src/widgets/message_row/default_user_name.dart'; 40 | part 'src/widgets/message_row/media_container.dart'; 41 | part 'src/widgets/message_row/message_row.dart'; 42 | part 'src/widgets/message_row/text_container.dart'; 43 | part 'src/widgets/message_row/video_player.dart'; 44 | part 'src/widgets/quick_replies/default_quick_reply.dart'; 45 | part 'src/widgets/quick_replies/quick_replies.dart'; 46 | part 'src/widgets/typing_users/default_typing_builder.dart'; 47 | part 'src/widgets/typing_users/typing_indicator.dart'; 48 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 13 | 17 | 21 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /lib/src/widgets/typing_users/typing_indicator.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// @nodoc 4 | class TypingIndicator extends StatefulWidget { 5 | const TypingIndicator({ 6 | Key? key, 7 | this.flashingCircleDarkColor = const Color(0xFF333333), 8 | this.flashingCircleBrightColor = const Color(0xFFaec1dd), 9 | }) : super(key: key); 10 | 11 | /// Dark color in the animation 12 | final Color flashingCircleDarkColor; 13 | 14 | /// Light color in the animation 15 | final Color flashingCircleBrightColor; 16 | 17 | @override 18 | State createState() => TypingIndicatorState(); 19 | } 20 | 21 | class TypingIndicatorState extends State 22 | with TickerProviderStateMixin { 23 | late AnimationController _repeatingController; 24 | final List _dotIntervals = const [ 25 | Interval(0.25, 0.8), 26 | Interval(0.35, 0.9), 27 | Interval(0.45, 1.0), 28 | ]; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | _repeatingController = AnimationController( 34 | vsync: this, 35 | duration: const Duration(milliseconds: 1200), 36 | ); 37 | _repeatingController.repeat(); 38 | } 39 | 40 | @override 41 | void dispose() { 42 | _repeatingController.dispose(); 43 | super.dispose(); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return SizedBox( 49 | width: 30, 50 | height: 15, 51 | child: Row( 52 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 53 | children: [ 54 | _buildFlashingCircle(0), 55 | _buildFlashingCircle(1), 56 | _buildFlashingCircle(2), 57 | ], 58 | ), 59 | ); 60 | } 61 | 62 | Widget _buildFlashingCircle(int index) { 63 | return AnimatedBuilder( 64 | animation: _repeatingController, 65 | builder: (BuildContext context, Widget? child) { 66 | final double circleFlashPercent = 67 | _dotIntervals[index].transform(_repeatingController.value); 68 | final double circleColorPercent = sin(pi * circleFlashPercent); 69 | 70 | return Container( 71 | width: 5, 72 | height: 5, 73 | decoration: BoxDecoration( 74 | shape: BoxShape.circle, 75 | color: Color.lerp(widget.flashingCircleDarkColor, 76 | widget.flashingCircleBrightColor, circleColorPercent), 77 | ), 78 | ); 79 | }, 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.21] - 02/06/2024 2 | 3 | * Upgraded dependencies 4 | * Added support for markdown stylesheet 5 | * Fixed defaultSendButton not using the icon param 6 | 7 | ## [0.0.20] - 01/02/2024 8 | 9 | * Markdown support 10 | * Fixed issue with mentions, see [#67](https://github.com/SebastienBtr/Dash-Chat-2/issues/67) 11 | * Fixed issue with videos, see [#70](https://github.com/SebastienBtr/Dash-Chat-2/issues/70) 12 | * Overall maintenance and minor code improvements 13 | 14 | ## [0.0.19] - 23/10/2023 15 | 16 | * More parse pattern by default 17 | * Fixed typo for defaultParsePatterns variable 18 | 19 | ## [0.0.18] - 22/08/2023 20 | 21 | * Allowed customProperties to be null 22 | * Upgrade dependencies 23 | 24 | ## [0.0.17] - 16/05/2023 25 | 26 | * Made it compatible with Flutter 3.10 27 | * Added more customisation on MessageOption and improved code quality 28 | 29 | ## [0.0.16] - 10/05/2023 30 | 31 | * Upgraded dependencies 32 | * Removed top safe area padding for the input widget 33 | * Added possibility to add asset image as user icon 34 | * Added black background to videos 35 | 36 | ## [0.0.15] - 03/10/2022 37 | 38 | * Fixed opening dynamic links (deeplinks) 39 | * Fixed issue with video previews 40 | * Fixed issues related to the mention overlay 41 | 42 | ## [0.0.14] - 23/08/2022 43 | 44 | * Hid the mention overlay when the input toolbar is disposed 45 | 46 | ## [0.0.13] - 22/08/2022 47 | 48 | * Added ability to mention anything using any trigger (for instance: @username) 49 | * Added maxWidth message option 50 | 51 | ## [0.0.12] - 19/07/2022 52 | 53 | * Added new param to the video player 54 | 55 | ## [0.0.11] - 06/07/2022 56 | 57 | * Improved default send button padding 58 | 59 | ## [0.0.10] - 30/06/2022 60 | 61 | * Improved tap gesture area of default send button 62 | 63 | ## [0.0.9] - 29/06/2022 64 | 65 | * Improved tap gesture area of default send button 66 | 67 | ## [0.0.8] - 28/06/2022 68 | 69 | * Fixed `sendOnEnter` param not working 70 | 71 | ## [0.0.7] - 27/06/2022 72 | 73 | * Upgraded dependencies 74 | * Fixed example 75 | * Added `scrollPhysics` param in `MessageListOptions` 76 | 77 | ## [0.0.6] - 21/04/2022 78 | 79 | * Added flutter web support 80 | 81 | ## [0.0.4] - 03/03/2022 82 | 83 | * Fixed scrollController issue for onLoadEarlier 84 | * UI improvements 85 | 86 | ## [0.0.3] - 27/09/2021 87 | 88 | * Change better_player to video_player to have support for web 89 | 90 | ## [0.0.2] - 24/09/2021 91 | 92 | * Fix readme image issue 93 | * Add description in pubspec 94 | 95 | ## [0.0.1] - 24/09/2021 96 | 97 | * First version 98 | -------------------------------------------------------------------------------- /lib/src/dash_chat.dart: -------------------------------------------------------------------------------- 1 | part of '../dash_chat_2.dart'; 2 | 3 | /// {@category Entry point} 4 | class DashChat extends StatelessWidget { 5 | const DashChat({ 6 | required this.currentUser, 7 | required this.onSend, 8 | required this.messages, 9 | this.inputOptions = const InputOptions(), 10 | this.messageOptions = const MessageOptions(), 11 | this.messageListOptions = const MessageListOptions(), 12 | this.quickReplyOptions = const QuickReplyOptions(), 13 | this.scrollToBottomOptions = const ScrollToBottomOptions(), 14 | this.readOnly = false, 15 | this.typingUsers, 16 | Key? key, 17 | }) : super(key: key); 18 | 19 | /// The current user of the chat 20 | final ChatUser currentUser; 21 | 22 | /// Function to call when the user sends a message 23 | final void Function(ChatMessage message) onSend; 24 | 25 | /// List of messages visible in the chat 26 | final List messages; 27 | 28 | /// Options to customize the behaviour and design of the chat input 29 | final InputOptions inputOptions; 30 | 31 | /// Options to customize the behaviour and design of the messages 32 | final MessageOptions messageOptions; 33 | 34 | /// Options to customize the behaviour and design of the overall list of message 35 | final MessageListOptions messageListOptions; 36 | 37 | /// Options to customize the behaviour and design of the quick replies 38 | final QuickReplyOptions quickReplyOptions; 39 | 40 | /// Options to customize the behaviour and design of the scroll-to-bottom button 41 | final ScrollToBottomOptions scrollToBottomOptions; 42 | 43 | /// Option to make the chat read only, it will hide the input field 44 | final bool readOnly; 45 | 46 | /// List of users currently typing in the chat 47 | final List? typingUsers; 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | return Column( 52 | children: [ 53 | Expanded( 54 | child: MessageList( 55 | currentUser: currentUser, 56 | messages: messages, 57 | messageOptions: messageOptions, 58 | messageListOptions: messageListOptions, 59 | quickReplyOptions: quickReplyOptions, 60 | scrollToBottomOptions: scrollToBottomOptions, 61 | typingUsers: typingUsers, 62 | readOnly: readOnly, 63 | ), 64 | ), 65 | if (!readOnly) 66 | InputToolbar( 67 | inputOptions: inputOptions, 68 | currentUser: currentUser, 69 | onSend: onSend, 70 | ), 71 | ], 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/models/chat_media.dart: -------------------------------------------------------------------------------- 1 | part of '../../dash_chat_2.dart'; 2 | 3 | /// {@category Models} 4 | class ChatMedia { 5 | ChatMedia({ 6 | required this.url, 7 | required this.fileName, 8 | required this.type, 9 | this.isUploading = false, 10 | this.uploadedDate, 11 | this.customProperties, 12 | }); 13 | 14 | /// Create a ChatMedia instance from json data 15 | factory ChatMedia.fromJson(Map jsonData) { 16 | return ChatMedia( 17 | url: jsonData['url'].toString(), 18 | fileName: jsonData['fileName'].toString(), 19 | type: MediaType.parse(jsonData['type'].toString()), 20 | isUploading: jsonData['isUploading'] == true, 21 | uploadedDate: jsonData['uploadedDate'] != null 22 | ? DateTime.parse(jsonData['uploadedDate'].toString()).toLocal() 23 | : null, 24 | customProperties: jsonData['customProperties'] as Map?, 25 | ); 26 | } 27 | 28 | /// URL of the media, can local (will use FileImage) or remote (will use NetworkImage) 29 | String url; 30 | 31 | /// Name of the file that will be shown in some cases 32 | String fileName; 33 | 34 | /// Type of media 35 | MediaType type; 36 | 37 | /// If the media is still uploading, useful to add a visual feedback 38 | bool isUploading; 39 | 40 | /// Uploaded date of the media 41 | DateTime? uploadedDate; 42 | 43 | /// A list of custom properties to extend the existing ones 44 | /// in case you need to store more things. 45 | /// Can be useful to extend existing features 46 | Map? customProperties; 47 | 48 | /// Convert a ChatMedia into a json 49 | Map toJson() { 50 | return { 51 | 'url': url, 52 | 'type': type.toString(), 53 | 'fileName': fileName, 54 | 'isUploading': isUploading, 55 | 'uploadedDate': uploadedDate?.toUtc().toIso8601String(), 56 | 'customProperties': customProperties, 57 | }; 58 | } 59 | } 60 | 61 | class MediaType { 62 | const MediaType._internal(this._value); 63 | final String _value; 64 | 65 | @override 66 | String toString() => _value; 67 | 68 | static MediaType parse(String value) { 69 | switch (value) { 70 | case 'image': 71 | return MediaType.image; 72 | case 'video': 73 | return MediaType.video; 74 | case 'file': 75 | return MediaType.file; 76 | default: 77 | throw UnsupportedError('$value is not a valid MediaType'); 78 | } 79 | } 80 | 81 | static const MediaType image = MediaType._internal('image'); 82 | static const MediaType video = MediaType._internal('video'); 83 | static const MediaType file = MediaType._internal('file'); 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/widgets/message_row/video_player.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// @nodoc 4 | class VideoPlayer extends StatefulWidget { 5 | const VideoPlayer({ 6 | required this.url, 7 | this.aspectRatio = 1, 8 | this.canPlay = true, 9 | Key? key, 10 | }) : super(key: key); 11 | 12 | /// Link of the video 13 | final String url; 14 | 15 | /// The Aspect Ratio of the Video. Important to get the correct size of the video 16 | final double aspectRatio; 17 | 18 | /// If the video can be played 19 | final bool canPlay; 20 | 21 | @override 22 | State createState() => VideoPlayerState(); 23 | } 24 | 25 | class VideoPlayerState extends State { 26 | late vp.VideoPlayerController _controller; 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | _controller = vp.VideoPlayerController.networkUrl(Uri.parse(widget.url)) 32 | ..initialize().then((_) { 33 | // Ensure the first frame is shown after the video is initialized, 34 | // even before the play button has been pressed. 35 | setState(() {}); 36 | }); 37 | } 38 | 39 | @override 40 | void dispose() { 41 | super.dispose(); 42 | _controller.dispose(); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return _controller.value.isInitialized 48 | ? Container( 49 | color: Colors.black, 50 | child: Stack( 51 | alignment: _controller.value.isPlaying 52 | ? AlignmentDirectional.bottomStart 53 | : AlignmentDirectional.center, 54 | children: [ 55 | AspectRatio( 56 | aspectRatio: _controller.value.aspectRatio, 57 | child: vp.VideoPlayer(_controller), 58 | ), 59 | IconButton( 60 | iconSize: _controller.value.isPlaying ? 24 : 60, 61 | onPressed: widget.canPlay 62 | ? () { 63 | setState(() { 64 | _controller.value.isPlaying 65 | ? _controller.pause() 66 | : _controller.play(); 67 | }); 68 | } 69 | : null, 70 | icon: Icon( 71 | _controller.value.isPlaying 72 | ? Icons.pause 73 | : Icons.play_arrow, 74 | color: Colors.white, 75 | // size: 60, 76 | ), 77 | ), 78 | ], 79 | ), 80 | ) 81 | : Container(color: Colors.black); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:examples/samples/avatar.dart'; 2 | import 'package:examples/samples/basic.dart'; 3 | import 'package:examples/samples/media.dart'; 4 | import 'package:examples/samples/mention.dart'; 5 | import 'package:examples/samples/quick_replies_sample.dart'; 6 | import 'package:examples/samples/send_on_enter.dart'; 7 | import 'package:examples/samples/theming.dart'; 8 | import 'package:examples/samples/typing_users_sample.dart'; 9 | import 'package:flutter/material.dart'; 10 | 11 | void main() { 12 | runApp(MyApp()); 13 | } 14 | 15 | class MyApp extends StatelessWidget { 16 | // This widget is the root of your application. 17 | @override 18 | Widget build(BuildContext context) { 19 | return MaterialApp( 20 | title: 'Dash Chat Demo', 21 | theme: ThemeData( 22 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.tealAccent), 23 | ), 24 | home: MyHomePage(), 25 | ); 26 | } 27 | } 28 | 29 | class MyHomePage extends StatefulWidget { 30 | @override 31 | State createState() => MyHomePageState(); 32 | } 33 | 34 | class MyHomePageState extends State { 35 | @override 36 | Widget build(BuildContext context) { 37 | return Scaffold( 38 | appBar: AppBar( 39 | title: const Text('Dash Chat Demo'), 40 | ), 41 | body: Center( 42 | child: Column( 43 | mainAxisAlignment: MainAxisAlignment.center, 44 | children: [ 45 | ElevatedButton( 46 | onPressed: () => push(Basic()), 47 | child: const Text('Basic'), 48 | ), 49 | ElevatedButton( 50 | onPressed: () => push(Media()), 51 | child: const Text('Chat media'), 52 | ), 53 | ElevatedButton( 54 | onPressed: () => push(AvatarSample()), 55 | child: const Text('All user possibilities'), 56 | ), 57 | ElevatedButton( 58 | onPressed: () => push(QuickRepliesSample()), 59 | child: const Text('Quick replies'), 60 | ), 61 | ElevatedButton( 62 | onPressed: () => push(TypingUsersSample()), 63 | child: const Text('Typing users'), 64 | ), 65 | ElevatedButton( 66 | onPressed: () => push(SendOnEnter()), 67 | child: const Text('Send on enter'), 68 | ), 69 | ElevatedButton( 70 | onPressed: () => push(MentionSample()), 71 | child: const Text('Mention'), 72 | ), 73 | ElevatedButton( 74 | onPressed: () => push(ThemeSample()), 75 | child: const Text('Theming'), 76 | ), 77 | ], 78 | ), 79 | ), 80 | ); 81 | } 82 | 83 | void push(Widget page) { 84 | Navigator.of(context).push( 85 | MaterialPageRoute( 86 | builder: (BuildContext context) { 87 | return page; 88 | }, 89 | ), 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: examples 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.12.0 <3.0.0" 22 | 23 | dependencies: 24 | dash_chat_2: 25 | path: ../ 26 | flutter: 27 | sdk: flutter 28 | 29 | dev_dependencies: 30 | flutter_lints: ^2.0.1 31 | flutter_test: 32 | sdk: flutter 33 | 34 | # For information on the generic Dart part of this file, see the 35 | # following page: https://dart.dev/tools/pub/pubspec 36 | 37 | # The following section is specific to Flutter. 38 | flutter: 39 | 40 | # The following line ensures that the Material Icons font is 41 | # included with your application, so that you can use the icons in 42 | # the material Icons class. 43 | uses-material-design: true 44 | 45 | # To add assets to your application, add an assets section, like this: 46 | # assets: 47 | # - images/a_dot_burr.jpeg 48 | # - images/a_dot_ham.jpeg 49 | 50 | # An image asset can refer to one or more resolution-specific "variants", see 51 | # https://flutter.dev/assets-and-images/#resolution-aware. 52 | 53 | # For details regarding adding assets from package dependencies, see 54 | # https://flutter.dev/assets-and-images/#from-packages 55 | 56 | # To add custom fonts to your application, add a fonts section here, 57 | # in this "flutter" section. Each entry in this list should have a 58 | # "family" key with the font family name, and a "fonts" key with a 59 | # list giving the asset and other descriptors for the font. For 60 | # example: 61 | # fonts: 62 | # - family: Schyler 63 | # fonts: 64 | # - asset: fonts/Schyler-Regular.ttf 65 | # - asset: fonts/Schyler-Italic.ttf 66 | # style: italic 67 | # - family: Trajan Pro 68 | # fonts: 69 | # - asset: fonts/TrajanPro.ttf 70 | # - asset: fonts/TrajanPro_Bold.ttf 71 | # weight: 700 72 | # 73 | # For details regarding fonts from package dependencies, 74 | # see https://flutter.dev/custom-fonts/#from-packages 75 | -------------------------------------------------------------------------------- /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 | analyzer: 13 | language: 14 | strict-casts: true 15 | strict-raw-types: true 16 | 17 | linter: 18 | # The lint rules applied to this project can be customized in the 19 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 20 | # included above or to enable additional rules. A list of all available lints 21 | # and their documentation is published at 22 | # https://dart-lang.github.io/linter/lints/index.html. 23 | # 24 | # Instead of disabling a lint rule for the entire project in the 25 | # section below, it can also be suppressed for a single line of code 26 | # or a specific dart file by using the `// ignore: name_of_lint` and 27 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 28 | # producing the lint. 29 | rules: 30 | always_declare_return_types: true 31 | always_put_control_body_on_new_line: true 32 | always_put_required_named_parameters_first: true 33 | always_specify_types: true 34 | avoid_bool_literals_in_conditional_expressions: true 35 | avoid_empty_else: true 36 | avoid_slow_async_io: true 37 | avoid_types_as_parameter_names: true 38 | avoid_unused_constructor_parameters: true 39 | avoid_void_async: true 40 | await_only_futures: true 41 | camel_case_extensions: true 42 | camel_case_types: true 43 | cancel_subscriptions: true 44 | collection_methods_unrelated_type: true 45 | directives_ordering: true 46 | empty_catches: true 47 | no_duplicate_case_values: true 48 | non_constant_identifier_names: true 49 | package_api_docs: true 50 | prefer_final_in_for_each: true 51 | prefer_if_elements_to_conditional_expressions: true 52 | prefer_is_empty: true 53 | prefer_is_not_empty: true 54 | prefer_relative_imports: true 55 | prefer_single_quotes: true 56 | prefer_typing_uninitialized_variables: true 57 | sort_constructors_first: true 58 | sort_pub_dependencies: true 59 | sort_unnamed_constructors_first: true 60 | test_types_in_equals: true 61 | throw_in_finally: true 62 | unnecessary_null_aware_assignments: true 63 | unnecessary_overrides: true 64 | unnecessary_parenthesis: true 65 | unnecessary_statements: true 66 | unrelated_type_equality_checks: true 67 | valid_regexps: true 68 | void_checks: true 69 | 70 | use_key_in_widget_constructors: false 71 | constant_identifier_names: false 72 | file_names: false 73 | 74 | # Additional information about this file can be found at 75 | # https://dart.dev/guides/language/analysis-options 76 | -------------------------------------------------------------------------------- /lib/src/widgets/message_list/default_scroll_to_bottom.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// {@category Default widgets} 4 | class DefaultScrollToBottom extends StatelessWidget { 5 | const DefaultScrollToBottom({ 6 | required this.scrollController, 7 | this.readOnly = false, 8 | this.backgroundColor, 9 | this.textColor, 10 | this.bottom = 10.0, 11 | this.left = 0.0, 12 | this.right = 0.0, 13 | this.top, 14 | this.height = 30.0, 15 | this.width = 30.0, 16 | this.elevation = 5, 17 | this.icon = Icons.arrow_downward, 18 | this.iconSize = 18, 19 | this.onScrollToBottomPress, 20 | Key? key, 21 | }) : super(key: key); 22 | 23 | /// Scroll controller of the chat list 24 | final ScrollController scrollController; 25 | 26 | /// Background color of the button 27 | final Color? backgroundColor; 28 | 29 | /// Icon color of the button 30 | final Color? textColor; 31 | 32 | /// The distance that the child's bottom edge is inset from the bottom of the stack 33 | final double? bottom; 34 | 35 | /// The distance that the child's left edge is inset from the left of the stack 36 | final double? left; 37 | 38 | /// The distance that the child's right edge is inset from the right of the stack 39 | final double? right; 40 | 41 | /// The distance that the child's top edge is inset from the top of the stack 42 | final double? top; 43 | 44 | /// Height of the button 45 | final double height; 46 | 47 | /// Width of the button 48 | final double width; 49 | 50 | /// Elevation of the button 51 | final double elevation; 52 | 53 | /// Icon of the button 54 | final IconData icon; 55 | 56 | /// Icon size 57 | final double iconSize; 58 | 59 | /// Function to call when the scroll-to-bottom widget is pressed 60 | /// It will scroll down in any case 61 | final void Function()? onScrollToBottomPress; 62 | 63 | /// Whether the chat is read only, used to add safe area padding to bottom 64 | final bool readOnly; 65 | 66 | @override 67 | Widget build(BuildContext context) { 68 | return Positioned( 69 | right: right, 70 | left: left, 71 | top: top, 72 | bottom: readOnly 73 | ? MediaQuery.of(context).padding.bottom + (bottom ?? 0) 74 | : bottom, 75 | child: SizedBox( 76 | width: width, 77 | height: height, 78 | child: RawMaterialButton( 79 | elevation: elevation, 80 | fillColor: backgroundColor, 81 | shape: const CircleBorder(), 82 | child: Icon( 83 | icon, 84 | size: iconSize, 85 | color: textColor, 86 | ), 87 | onPressed: () { 88 | if (onScrollToBottomPress != null) { 89 | onScrollToBottomPress!(); 90 | } 91 | scrollController.animateTo( 92 | 0.0, 93 | duration: const Duration(milliseconds: 300), 94 | curve: Curves.easeInOut, 95 | ); 96 | }, 97 | ), 98 | ), 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "SebastienBtr", 10 | "name": "SebastienBtr", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/18089010?v=4", 12 | "profile": "https://github.com/SebastienBtr", 13 | "contributions": [ 14 | "code", 15 | "design" 16 | ] 17 | }, 18 | { 19 | "login": "chuusungmin", 20 | "name": "chuusungmin", 21 | "avatar_url": "https://avatars.githubusercontent.com/u/17997403?v=4", 22 | "profile": "https://github.com/chuusungmin", 23 | "contributions": [ 24 | "code" 25 | ] 26 | }, 27 | { 28 | "login": "fufesou", 29 | "name": "fufesou", 30 | "avatar_url": "https://avatars.githubusercontent.com/u/13586388?v=4", 31 | "profile": "https://github.com/fufesou", 32 | "contributions": [ 33 | "code" 34 | ] 35 | }, 36 | { 37 | "login": "farmery", 38 | "name": "Nwachi ifeanyichukwu Victor", 39 | "avatar_url": "https://avatars.githubusercontent.com/u/56759256?v=4", 40 | "profile": "https://github.com/farmery", 41 | "contributions": [ 42 | "code" 43 | ] 44 | }, 45 | { 46 | "login": "kaedeee", 47 | "name": "Kaede Games", 48 | "avatar_url": "https://avatars.githubusercontent.com/u/55743370?v=4", 49 | "profile": "https://github.com/kaedeee", 50 | "contributions": [ 51 | "code" 52 | ] 53 | }, 54 | { 55 | "login": "derekpitts28", 56 | "name": "Derek Pitts", 57 | "avatar_url": "https://avatars.githubusercontent.com/u/83979577?v=4", 58 | "profile": "https://github.com/derekpitts28", 59 | "contributions": [ 60 | "code" 61 | ] 62 | }, 63 | { 64 | "login": "LegendAF", 65 | "name": "Alex Fernandez", 66 | "avatar_url": "https://avatars.githubusercontent.com/u/825344?v=4", 67 | "profile": "https://github.com/LegendAF", 68 | "contributions": [ 69 | "code" 70 | ] 71 | }, 72 | { 73 | "login": "funjay", 74 | "name": "lawrence", 75 | "avatar_url": "https://avatars.githubusercontent.com/u/66911451?v=4", 76 | "profile": "https://github.com/funjay", 77 | "contributions": [ 78 | "code" 79 | ] 80 | }, 81 | { 82 | "login": "alamin-karno", 83 | "name": "Md. Al-Amin", 84 | "avatar_url": "https://avatars.githubusercontent.com/u/56608168?v=4", 85 | "profile": "https://github.com/alamin-karno", 86 | "contributions": [ 87 | "code" 88 | ] 89 | }, 90 | { 91 | "login": "T-P-F", 92 | "name": "TPF", 93 | "avatar_url": "https://avatars.githubusercontent.com/u/61667947?v=4", 94 | "profile": "https://github.com/T-P-F", 95 | "contributions": [ 96 | "code" 97 | ] 98 | } 99 | ], 100 | "contributorsPerLine": 7, 101 | "projectName": "Dash-Chat-2", 102 | "projectOwner": "SebastienBtr", 103 | "repoType": "github", 104 | "repoHost": "https://github.com", 105 | "skipCi": true, 106 | "commitConvention": "angular", 107 | "commitType": "docs" 108 | } 109 | -------------------------------------------------------------------------------- /lib/src/widgets/message_row/text_container.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// @nodoc 4 | class TextContainer extends StatelessWidget { 5 | const TextContainer({ 6 | required this.message, 7 | this.messageOptions = const MessageOptions(), 8 | this.previousMessage, 9 | this.nextMessage, 10 | this.isOwnMessage = false, 11 | this.isPreviousSameAuthor = false, 12 | this.isNextSameAuthor = false, 13 | this.isAfterDateSeparator = false, 14 | this.isBeforeDateSeparator = false, 15 | this.messageTextBuilder, 16 | Key? key, 17 | }) : super(key: key); 18 | 19 | /// Options to customize the behaviour and design of the messages 20 | final MessageOptions messageOptions; 21 | 22 | /// Message that contains the text to show 23 | final ChatMessage message; 24 | 25 | /// Previous message in the list 26 | final ChatMessage? previousMessage; 27 | 28 | /// Next message in the list 29 | final ChatMessage? nextMessage; 30 | 31 | /// If the message is from the current user 32 | final bool isOwnMessage; 33 | 34 | /// If the previous message is from the same author as the current one 35 | final bool isPreviousSameAuthor; 36 | 37 | /// If the next message is from the same author as the current one 38 | final bool isNextSameAuthor; 39 | 40 | /// If the message is preceded by a date separator 41 | final bool isAfterDateSeparator; 42 | 43 | /// If the message is before by a date separator 44 | final bool isBeforeDateSeparator; 45 | 46 | /// We could access that from messageOptions but we want to reuse this widget 47 | /// for media and be able to override the text builder 48 | final Widget Function(ChatMessage, ChatMessage?, ChatMessage?)? 49 | messageTextBuilder; 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return Container( 54 | decoration: messageOptions.messageDecorationBuilder != null 55 | ? messageOptions.messageDecorationBuilder!( 56 | message, previousMessage, nextMessage) 57 | : defaultMessageDecoration( 58 | color: isOwnMessage 59 | ? messageOptions.currentUserContainerColor(context) 60 | : messageOptions.containerColor, 61 | borderTopLeft: 62 | isPreviousSameAuthor && !isOwnMessage && !isAfterDateSeparator 63 | ? 0.0 64 | : messageOptions.borderRadius, 65 | borderTopRight: 66 | isPreviousSameAuthor && isOwnMessage && !isAfterDateSeparator 67 | ? 0.0 68 | : messageOptions.borderRadius, 69 | borderBottomLeft: 70 | !isOwnMessage && !isBeforeDateSeparator && isNextSameAuthor 71 | ? 0.0 72 | : messageOptions.borderRadius, 73 | borderBottomRight: 74 | isOwnMessage && !isBeforeDateSeparator && isNextSameAuthor 75 | ? 0.0 76 | : messageOptions.borderRadius, 77 | ), 78 | padding: messageOptions.messagePadding, 79 | child: messageTextBuilder != null 80 | ? messageTextBuilder!(message, previousMessage, nextMessage) 81 | : DefaultMessageText( 82 | message: message, 83 | isOwnMessage: isOwnMessage, 84 | messageOptions: messageOptions, 85 | ), 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example/lib/samples/theming.dart: -------------------------------------------------------------------------------- 1 | import 'package:dash_chat_2/dash_chat_2.dart'; 2 | import 'package:examples/data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class ThemeSample extends StatefulWidget { 6 | @override 7 | State createState() => ThemeSampleState(); 8 | } 9 | 10 | class ThemeSampleState extends State { 11 | List messages = basicSample; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Theme( 16 | data: ThemeData( 17 | appBarTheme: const AppBarTheme( 18 | color: Color(0xff00ff9f), 19 | titleTextStyle: TextStyle( 20 | color: Color(0xff001eff), 21 | fontSize: 20, 22 | fontWeight: FontWeight.bold, 23 | ), 24 | iconTheme: IconThemeData( 25 | color: Color(0xff001eff), 26 | ), 27 | ), 28 | ), 29 | child: Scaffold( 30 | appBar: AppBar( 31 | title: const Text('Theming Example'), 32 | ), 33 | body: Container( 34 | color: const Color(0xff001eff), 35 | child: DashChat( 36 | currentUser: user, 37 | onSend: (ChatMessage m) { 38 | setState(() { 39 | messages.insert(0, m); 40 | }); 41 | }, 42 | inputOptions: InputOptions( 43 | inputTextStyle: const TextStyle( 44 | color: Color(0xff001eff), 45 | ), 46 | inputDecoration: InputDecoration( 47 | isDense: true, 48 | filled: true, 49 | fillColor: const Color(0xff00b8ff), 50 | contentPadding: const EdgeInsets.only( 51 | left: 18, 52 | top: 10, 53 | bottom: 10, 54 | ), 55 | border: OutlineInputBorder( 56 | borderRadius: BorderRadius.circular(25), 57 | borderSide: const BorderSide( 58 | width: 0, 59 | style: BorderStyle.none, 60 | ), 61 | ), 62 | ), 63 | ), 64 | messages: messages, 65 | messageOptions: const MessageOptions( 66 | containerColor: Color(0xffd600ff), 67 | currentUserContainerColor: Color(0xffbd00ff), 68 | currentUserTextColor: Color(0xfffbf665), 69 | currentUserTimeTextColor: Color.fromRGBO(73, 0, 100, 1), 70 | messagePadding: EdgeInsets.fromLTRB(12, 8, 12, 8), 71 | showTime: true, 72 | spaceWhenAvatarIsHidden: 6, 73 | textColor: Color(0xfffbf665), 74 | timeFontSize: 8, 75 | timePadding: EdgeInsets.only(top: 2), 76 | timeTextColor: Color.fromRGBO(73, 0, 100, 1), 77 | ), 78 | messageListOptions: MessageListOptions( 79 | dateSeparatorBuilder: (date) => DefaultDateSeparator( 80 | date: date, 81 | textStyle: const TextStyle( 82 | color: Color(0xff00ff9f), 83 | fontWeight: FontWeight.bold, 84 | ), 85 | ), 86 | onLoadEarlier: () async { 87 | await Future.delayed(const Duration(seconds: 3)); 88 | }, 89 | ), 90 | ), 91 | ), 92 | ), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/src/widgets/message_row/default_avatar.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// {@category Default widgets} 4 | class DefaultAvatar extends StatelessWidget { 5 | const DefaultAvatar({ 6 | required this.user, 7 | this.size = 35, 8 | this.fallbackImage, 9 | this.onPressAvatar, 10 | this.onLongPressAvatar, 11 | }); 12 | 13 | /// The URL of the user's profile picture 14 | final ChatUser user; 15 | 16 | /// Size of the avatar 17 | final double size; 18 | 19 | /// Placeholder image in case there is no initials ot he profile image do not load 20 | final ImageProvider? fallbackImage; 21 | 22 | /// Function to call when the user long press on the avatar 23 | final void Function(ChatUser)? onLongPressAvatar; 24 | 25 | /// Function to call when the user press on the avatar 26 | final void Function(ChatUser)? onPressAvatar; 27 | 28 | /// Get the initials of the user 29 | String getInitials() { 30 | return (user.firstName == null || user.firstName!.isEmpty 31 | ? '' 32 | : user.firstName![0]) + 33 | (user.lastName == null || user.lastName!.isEmpty 34 | ? '' 35 | : user.lastName![0]); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | return GestureDetector( 41 | onTap: onPressAvatar != null ? () => onPressAvatar!(user) : null, 42 | onLongPress: 43 | onLongPressAvatar != null ? () => onLongPressAvatar!(user) : null, 44 | child: Padding( 45 | padding: const EdgeInsets.symmetric( 46 | horizontal: 10, 47 | ), 48 | child: SizedBox( 49 | height: size, 50 | width: size, 51 | child: Stack( 52 | alignment: Alignment.center, 53 | children: [ 54 | ClipOval( 55 | child: Container( 56 | color: Colors.grey[200], 57 | child: getInitials().isNotEmpty 58 | ? Center( 59 | child: Text( 60 | getInitials(), 61 | style: TextStyle( 62 | color: Colors.black, 63 | fontSize: size * 0.35, 64 | ), 65 | ), 66 | ) 67 | : Image( 68 | image: fallbackImage ?? 69 | const AssetImage( 70 | 'assets/profile_placeholder.png', 71 | package: 'dash_chat_2', 72 | ), 73 | ), 74 | ), 75 | ), 76 | if (user.profileImage != null && user.profileImage!.isNotEmpty) 77 | Center( 78 | child: ClipOval( 79 | child: FadeInImage( 80 | width: size, 81 | height: size, 82 | fit: BoxFit.cover, 83 | image: getImageProvider(user.profileImage!), 84 | placeholder: fallbackImage ?? 85 | const AssetImage( 86 | 'assets/profile_placeholder.png', 87 | package: 'dash_chat_2', 88 | ), 89 | ), 90 | ), 91 | ), 92 | ], 93 | ), 94 | ), 95 | ), 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /example/lib/samples/mention.dart: -------------------------------------------------------------------------------- 1 | import 'package:dash_chat_2/dash_chat_2.dart'; 2 | import 'package:examples/data.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class MentionSample extends StatefulWidget { 6 | @override 7 | State createState() => MentionSampleState(); 8 | } 9 | 10 | class MentionSampleState extends State { 11 | List messages = mentionSample; 12 | List mentions = []; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: AppBar( 18 | title: const Text('Mention example'), 19 | ), 20 | body: DashChat( 21 | currentUser: user, 22 | onSend: (ChatMessage m) { 23 | m.mentions = mentions; 24 | setState(() { 25 | messages.insert(0, m); 26 | mentions = []; 27 | }); 28 | }, 29 | messages: messages, 30 | messageListOptions: MessageListOptions( 31 | onLoadEarlier: () async { 32 | await Future.delayed(const Duration(seconds: 3)); 33 | }, 34 | ), 35 | messageOptions: MessageOptions( 36 | onPressMention: (mention) { 37 | showDialog( 38 | context: context, 39 | builder: (BuildContext context) { 40 | return AlertDialog( 41 | title: Text(mention.title), 42 | content: Text(mention.customProperties.toString()), 43 | ); 44 | }, 45 | ); 46 | }, 47 | ), 48 | inputOptions: InputOptions( 49 | onMention: (String trigger, String value, 50 | void Function(String) onMentionClick) { 51 | // Here you would typically do a request to your backend 52 | // to get the correct results to show 53 | // according to the trigger and value 54 | return Future.delayed( 55 | const Duration(milliseconds: 500), 56 | () { 57 | return [ 58 | ListTile( 59 | leading: DefaultAvatar(user: user8), 60 | title: Text(user8.getFullName()), 61 | onTap: () { 62 | onMentionClick(user8.getFullName()); 63 | mentions.add( 64 | Mention( 65 | title: trigger + user8.getFullName(), 66 | customProperties: { 67 | 'userId': user8.id, 68 | }, 69 | ), 70 | ); 71 | }, 72 | ), 73 | const Divider(), 74 | ListTile( 75 | leading: DefaultAvatar(user: user5), 76 | title: Text(user5.getFullName()), 77 | onTap: () { 78 | onMentionClick(user5.getFullName()); 79 | mentions.add( 80 | Mention( 81 | title: trigger + user5.getFullName(), 82 | customProperties: { 83 | 'userId': user5.id, 84 | }, 85 | ), 86 | ); 87 | }, 88 | ) 89 | ]; 90 | }, 91 | ); 92 | }, 93 | onMentionTriggers: ['@', '#'], 94 | ), 95 | ), 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | examples 27 | 28 | 29 | 30 | 33 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /lib/src/models/input_options.dart: -------------------------------------------------------------------------------- 1 | part of '../../dash_chat_2.dart'; 2 | 3 | /// {@category Customization} 4 | class InputOptions { 5 | const InputOptions({ 6 | this.textController, 7 | this.focusNode, 8 | this.inputTextDirection = TextDirection.ltr, 9 | this.onMention, 10 | this.onMentionTriggers = const ['@'], 11 | this.onTextChange, 12 | this.inputDisabled = false, 13 | this.inputDecoration, 14 | this.textCapitalization = TextCapitalization.none, 15 | this.alwaysShowSend = false, 16 | this.sendOnEnter = false, 17 | this.textInputAction, 18 | this.maxInputLength, 19 | this.leading, 20 | this.trailing, 21 | this.sendButtonBuilder, 22 | this.inputTextStyle, 23 | this.inputToolbarStyle, 24 | this.inputMaxLines = 5, 25 | this.showTraillingBeforeSend = false, 26 | this.inputToolbarPadding = const EdgeInsets.all(8.0), 27 | this.inputToolbarMargin = const EdgeInsets.only(top: 8.0), 28 | this.cursorStyle = const CursorStyle(), 29 | this.autocorrect = true, 30 | }); 31 | 32 | /// Function to call when a mention is triggered in the input, 33 | /// ie: typing ' @' 34 | /// You need to return a list of widget that will be shown inside the selection overlay, 35 | /// for instance user ListTiles 36 | final Future> Function(String trigger, String value, 37 | void Function(String value) onMentionClick)? onMention; 38 | 39 | /// The list of string triggers for the onMention callback 40 | /// By default it only includes '@' character 41 | final List onMentionTriggers; 42 | 43 | /// Function to call when the input text change 44 | final void Function(String value)? onTextChange; 45 | 46 | /// Always show the send button, will be hidden when the text is empty otherwise 47 | final bool alwaysShowSend; 48 | 49 | /// Send the message when the user presses the enter key 50 | final bool sendOnEnter; 51 | 52 | /// Builder to create your own send button widget 53 | /// You can use defaultSendButton to only override some variables 54 | final Widget Function(void Function() send)? sendButtonBuilder; 55 | 56 | /// Text controller for the input field 57 | final TextEditingController? textController; 58 | 59 | /// Focus node of the input field 60 | final FocusNode? focusNode; 61 | 62 | /// Use to change the direction of the text 63 | final TextDirection inputTextDirection; 64 | 65 | /// To make the input disabled 66 | final bool inputDisabled; 67 | 68 | /// Input decoration to customize the design of the input 69 | /// You can use defaultInputDecoration to only override some variables 70 | final InputDecoration? inputDecoration; 71 | 72 | /// Use to override the default TextCapitalization 73 | final TextCapitalization textCapitalization; 74 | 75 | /// An action the user has requested the text input control to perform 76 | final TextInputAction? textInputAction; 77 | 78 | /// If you want to limit the length of the text 79 | final int? maxInputLength; 80 | 81 | /// A list of widget to show before the input 82 | final List? leading; 83 | 84 | /// A list of widget to show after the input 85 | final List? trailing; 86 | 87 | /// To customize the text style of the input 88 | final TextStyle? inputTextStyle; 89 | 90 | /// To customize the overall container of the input 91 | final BoxDecoration? inputToolbarStyle; 92 | 93 | /// Max number of visible lines of the input, it will grow until this value and then scroll 94 | final int inputMaxLines; 95 | 96 | /// If [trailing] should be shown before or after the send button 97 | final bool showTraillingBeforeSend; 98 | 99 | /// Padding of the overall container of the input 100 | final EdgeInsets? inputToolbarPadding; 101 | 102 | /// Margin of the overall container of the input 103 | final EdgeInsets? inputToolbarMargin; 104 | 105 | /// Style of the cursor 106 | final CursorStyle cursorStyle; 107 | 108 | /// Whether to enable auto-correction. Defaults to true. 109 | final bool autocorrect; 110 | } 111 | -------------------------------------------------------------------------------- /lib/src/models/chat_message.dart: -------------------------------------------------------------------------------- 1 | part of '../../dash_chat_2.dart'; 2 | 3 | /// {@category Models} 4 | class ChatMessage { 5 | ChatMessage({ 6 | required this.user, 7 | required this.createdAt, 8 | this.isMarkdown = false, 9 | this.text = '', 10 | this.medias, 11 | this.quickReplies, 12 | this.customProperties, 13 | this.mentions, 14 | this.status = MessageStatus.none, 15 | this.replyTo, 16 | }); 17 | 18 | /// Create a ChatMessage instance from json data 19 | factory ChatMessage.fromJson(Map jsonData) { 20 | return ChatMessage( 21 | user: ChatUser.fromJson(jsonData['user'] as Map), 22 | createdAt: DateTime.parse(jsonData['createdAt'].toString()).toLocal(), 23 | text: jsonData['text']?.toString() ?? '', 24 | isMarkdown: jsonData['isMarkdown']?.toString() == 'true', 25 | medias: jsonData['medias'] != null 26 | ? (jsonData['medias'] as List) 27 | .map((dynamic media) => 28 | ChatMedia.fromJson(media as Map)) 29 | .toList() 30 | : [], 31 | quickReplies: jsonData['quickReplies'] != null 32 | ? (jsonData['quickReplies'] as List) 33 | .map((dynamic quickReply) => 34 | QuickReply.fromJson(quickReply as Map)) 35 | .toList() 36 | : [], 37 | customProperties: jsonData['customProperties'] as Map?, 38 | mentions: jsonData['mentions'] != null 39 | ? (jsonData['mentions'] as List) 40 | .map((dynamic mention) => 41 | Mention.fromJson(mention as Map)) 42 | .toList() 43 | : [], 44 | status: MessageStatus.parse(jsonData['status'].toString()), 45 | replyTo: jsonData['replyTo'] != null 46 | ? ChatMessage.fromJson(jsonData['replyTo'] as Map) 47 | : null, 48 | ); 49 | } 50 | 51 | /// If the message is Markdown formatted then it will be converted to Markdown (by default it will be false) 52 | bool isMarkdown; 53 | 54 | /// Text of the message (optional because you can also just send a media) 55 | String text; 56 | 57 | /// Author of the message 58 | ChatUser user; 59 | 60 | /// List of medias of the message 61 | List? medias; 62 | 63 | /// A list of quick replies that users can use to reply to this message 64 | List? quickReplies; 65 | 66 | /// A list of custom properties to extend the existing ones 67 | /// in case you need to store more things. 68 | /// Can be useful to extend existing features 69 | Map? customProperties; 70 | 71 | /// Date of the message 72 | DateTime createdAt; 73 | 74 | /// Mentioned elements in the message 75 | List? mentions; 76 | 77 | /// Status of the message TODO: 78 | MessageStatus? status; 79 | 80 | /// If the message is a reply of another one TODO: 81 | ChatMessage? replyTo; 82 | 83 | /// Convert a ChatMessage into a json 84 | Map toJson() { 85 | return { 86 | 'user': user.toJson(), 87 | 'createdAt': createdAt.toUtc().toIso8601String(), 88 | 'text': text, 89 | 'medias': medias?.map((ChatMedia media) => media.toJson()).toList(), 90 | 'quickReplies': quickReplies 91 | ?.map((QuickReply quickReply) => quickReply.toJson()) 92 | .toList(), 93 | 'customProperties': customProperties, 94 | 'mentions': mentions, 95 | 'status': status.toString(), 96 | 'replyTo': replyTo?.toJson(), 97 | 'isMarkdown': isMarkdown, 98 | }; 99 | } 100 | } 101 | 102 | class MessageStatus { 103 | const MessageStatus._internal(this._value); 104 | final String _value; 105 | 106 | @override 107 | String toString() => _value; 108 | 109 | static MessageStatus parse(String value) { 110 | switch (value) { 111 | case 'none': 112 | return MessageStatus.none; 113 | case 'failed': 114 | return MessageStatus.failed; 115 | case 'sent': 116 | return MessageStatus.sent; 117 | case 'read': 118 | return MessageStatus.read; 119 | case 'received': 120 | return MessageStatus.received; 121 | case 'pending': 122 | return MessageStatus.pending; 123 | default: 124 | return MessageStatus.none; 125 | } 126 | } 127 | 128 | static const MessageStatus none = MessageStatus._internal('none'); 129 | static const MessageStatus failed = MessageStatus._internal('failed'); 130 | static const MessageStatus sent = MessageStatus._internal('sent'); 131 | static const MessageStatus read = MessageStatus._internal('read'); 132 | static const MessageStatus received = MessageStatus._internal('received'); 133 | static const MessageStatus pending = MessageStatus._internal('pending'); 134 | } 135 | -------------------------------------------------------------------------------- /lib/src/widgets/message_row/default_message_text.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// {@category Default widgets} 4 | class DefaultMessageText extends StatelessWidget { 5 | const DefaultMessageText({ 6 | required this.message, 7 | required this.isOwnMessage, 8 | this.messageOptions = const MessageOptions(), 9 | Key? key, 10 | }) : super(key: key); 11 | 12 | /// Message tha contains the text to show 13 | final ChatMessage message; 14 | 15 | /// If the message is from the current user 16 | final bool isOwnMessage; 17 | 18 | /// Options to customize the behaviour and design of the messages 19 | final MessageOptions messageOptions; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Column( 24 | crossAxisAlignment: 25 | isOwnMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, 26 | children: [ 27 | Wrap( 28 | children: getMessage(context), 29 | ), 30 | if (messageOptions.showTime) 31 | messageOptions.messageTimeBuilder != null 32 | ? messageOptions.messageTimeBuilder!(message, isOwnMessage) 33 | : Padding( 34 | padding: messageOptions.timePadding, 35 | child: Text( 36 | (messageOptions.timeFormat ?? intl.DateFormat('HH:mm')) 37 | .format(message.createdAt), 38 | style: TextStyle( 39 | color: isOwnMessage 40 | ? messageOptions.currentUserTimeTextColor(context) 41 | : messageOptions.timeTextColor(), 42 | fontSize: messageOptions.timeFontSize, 43 | ), 44 | ), 45 | ), 46 | ], 47 | ); 48 | } 49 | 50 | List getMessage(BuildContext context) { 51 | if (message.mentions != null && message.mentions!.isNotEmpty) { 52 | String stringRegex = r'([\s\S]*)'; 53 | String stringMentionRegex = ''; 54 | for (final Mention mention in message.mentions!) { 55 | stringRegex += '(${mention.title})' r'([\s\S]*)'; 56 | stringMentionRegex += stringMentionRegex.isEmpty 57 | ? '(${mention.title})' 58 | : '|(${mention.title})'; 59 | } 60 | final RegExp mentionRegex = RegExp(stringMentionRegex); 61 | final RegExp regexp = RegExp(stringRegex); 62 | 63 | RegExpMatch? match = regexp.firstMatch(message.text); 64 | if (match != null) { 65 | List res = []; 66 | match 67 | .groups(List.generate(match.groupCount, (int i) => i + 1)) 68 | .forEach((String? part) { 69 | Mention? mention; 70 | if (mentionRegex.hasMatch(part!)) { 71 | try { 72 | mention = message.mentions?.firstWhere( 73 | (Mention m) => m.title == part, 74 | ); 75 | } catch (e) { 76 | // There is no mention 77 | } 78 | } 79 | if (mention != null) { 80 | res.add(getMention(context, mention)); 81 | } else { 82 | res.add(getParsePattern(context, part, message.isMarkdown)); 83 | } 84 | }); 85 | if (res.isNotEmpty) { 86 | return res; 87 | } 88 | } 89 | } 90 | return [getParsePattern(context, message.text, message.isMarkdown)]; 91 | } 92 | 93 | Widget getParsePattern(BuildContext context, String text, bool isMarkdown) { 94 | return isMarkdown 95 | ? MarkdownBody( 96 | data: text, 97 | selectable: true, 98 | styleSheet: messageOptions.markdownStyleSheet, 99 | onTapLink: (String value, String? href, String title) { 100 | if (href != null) { 101 | openLink(href); 102 | } else { 103 | openLink(value); 104 | } 105 | }, 106 | ) 107 | : ParsedText( 108 | parse: messageOptions.parsePatterns != null 109 | ? messageOptions.parsePatterns! 110 | : defaultParsePatterns, 111 | text: text, 112 | style: TextStyle( 113 | color: isOwnMessage 114 | ? messageOptions.currentUserTextColor(context) 115 | : messageOptions.textColor, 116 | ), 117 | ); 118 | } 119 | 120 | Widget getMention(BuildContext context, Mention mention) { 121 | return RichText( 122 | text: TextSpan( 123 | text: mention.title, 124 | recognizer: TapGestureRecognizer() 125 | ..onTap = () => messageOptions.onPressMention != null 126 | ? messageOptions.onPressMention!(mention) 127 | : null, 128 | style: TextStyle( 129 | color: isOwnMessage 130 | ? messageOptions.currentUserTextColor(context) 131 | : messageOptions.textColor, 132 | decoration: TextDecoration.none, 133 | fontWeight: FontWeight.w600, 134 | ), 135 | ), 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/src/widgets/message_row/media_container.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// @nodoc 4 | class MediaContainer extends StatelessWidget { 5 | const MediaContainer({ 6 | required this.message, 7 | required this.isOwnMessage, 8 | this.messageOptions = const MessageOptions(), 9 | Key? key, 10 | }) : super(key: key); 11 | 12 | /// Message that contains the media to show 13 | final ChatMessage message; 14 | 15 | /// If the message is from the current user 16 | final bool isOwnMessage; 17 | 18 | /// Options to customize the behaviour and design of the messages 19 | final MessageOptions messageOptions; 20 | 21 | /// Get the right media widget according to its type 22 | Widget _getMedia( 23 | BuildContext context, 24 | ChatMedia media, 25 | double? height, 26 | double? width, 27 | ) { 28 | final Widget loading = Container( 29 | width: 15, 30 | height: 15, 31 | margin: const EdgeInsets.all(10), 32 | child: const CircularProgressIndicator(), 33 | ); 34 | switch (media.type) { 35 | case MediaType.video: 36 | return Stack( 37 | alignment: AlignmentDirectional.bottomEnd, 38 | children: [ 39 | VideoPlayer(url: media.url, key: Key(media.url)), 40 | if (media.isUploading) loading 41 | ], 42 | ); 43 | case MediaType.image: 44 | return Stack( 45 | alignment: AlignmentDirectional.bottomEnd, 46 | children: [ 47 | Image( 48 | height: height, 49 | width: width, 50 | fit: BoxFit.cover, 51 | alignment: isOwnMessage ? Alignment.topRight : Alignment.topLeft, 52 | image: getImageProvider(media.url), 53 | ), 54 | if (media.isUploading) loading 55 | ], 56 | ); 57 | default: 58 | return TextContainer( 59 | isOwnMessage: isOwnMessage, 60 | messageOptions: messageOptions, 61 | message: message, 62 | messageTextBuilder: (ChatMessage m, ChatMessage? p, ChatMessage? n) { 63 | return Row( 64 | children: [ 65 | Padding( 66 | padding: const EdgeInsets.only(right: 8.0), 67 | child: !media.isUploading 68 | ? Icon( 69 | Icons.description, 70 | size: 18, 71 | color: isOwnMessage 72 | ? messageOptions.currentUserTextColor(context) 73 | : messageOptions.textColor, 74 | ) 75 | : loading, 76 | ), 77 | Flexible( 78 | child: Text( 79 | media.fileName, 80 | style: TextStyle( 81 | decoration: TextDecoration.underline, 82 | color: isOwnMessage 83 | ? messageOptions.currentUserTextColor(context) 84 | : messageOptions.textColor, 85 | ), 86 | ), 87 | ), 88 | ], 89 | ); 90 | }, 91 | ); 92 | } 93 | } 94 | 95 | @override 96 | Widget build(BuildContext context) { 97 | if (message.medias != null && message.medias!.isNotEmpty) { 98 | final List media = message.medias!; 99 | return Wrap( 100 | alignment: isOwnMessage ? WrapAlignment.end : WrapAlignment.start, 101 | children: media.map( 102 | (ChatMedia m) { 103 | final double gallerySize = 104 | (MediaQuery.of(context).size.width * 0.7) / 2 - 5; 105 | final bool isImage = m.type == MediaType.image; 106 | return Container( 107 | color: Colors.transparent, 108 | margin: const EdgeInsets.only(top: 5, right: 5), 109 | width: media.length > 1 && isImage ? gallerySize : null, 110 | height: media.length > 1 && isImage ? gallerySize : null, 111 | constraints: BoxConstraints( 112 | maxHeight: MediaQuery.of(context).size.height * 0.5, 113 | maxWidth: MediaQuery.of(context).size.width * 0.7, 114 | ), 115 | child: GestureDetector( 116 | onTap: messageOptions.onTapMedia != null 117 | ? () => messageOptions.onTapMedia!(m) 118 | : null, 119 | child: ClipRRect( 120 | borderRadius: BorderRadius.circular(8.0), 121 | child: ColorFiltered( 122 | colorFilter: ColorFilter.mode( 123 | m.isUploading 124 | ? Colors.white54 125 | : Colors.white.withOpacity( 126 | 0.1, 127 | ), // Because transparent is causing an issue on flutter web 128 | BlendMode.srcATop, 129 | ), 130 | child: _getMedia( 131 | context, 132 | m, 133 | media.length > 1 ? gallerySize : null, 134 | media.length > 1 ? gallerySize : null, 135 | ), 136 | ), 137 | ), 138 | ), 139 | ); 140 | }, 141 | ).toList(), 142 | ); 143 | } 144 | return const SizedBox(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/src/widgets/message_row/message_row.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// @nodoc 4 | class MessageRow extends StatelessWidget { 5 | const MessageRow({ 6 | required this.message, 7 | required this.currentUser, 8 | this.previousMessage, 9 | this.nextMessage, 10 | this.isAfterDateSeparator = false, 11 | this.isBeforeDateSeparator = false, 12 | this.messageOptions = const MessageOptions(), 13 | Key? key, 14 | }) : super(key: key); 15 | 16 | /// Current message to show 17 | final ChatMessage message; 18 | 19 | /// Previous message in the list 20 | final ChatMessage? previousMessage; 21 | 22 | /// Next message in the list 23 | final ChatMessage? nextMessage; 24 | 25 | /// Current user of the chat 26 | final ChatUser currentUser; 27 | 28 | /// If the message is preceded by a date separator 29 | final bool isAfterDateSeparator; 30 | 31 | /// If the message is before a date separator 32 | final bool isBeforeDateSeparator; 33 | 34 | /// Options to customize the behaviour and design of the messages 35 | final MessageOptions messageOptions; 36 | 37 | /// Get the avatar widget 38 | Widget getAvatar() { 39 | return messageOptions.avatarBuilder != null 40 | ? messageOptions.avatarBuilder!( 41 | message.user, 42 | messageOptions.onPressAvatar, 43 | messageOptions.onLongPressAvatar, 44 | ) 45 | : DefaultAvatar( 46 | user: message.user, 47 | onLongPressAvatar: messageOptions.onLongPressAvatar, 48 | onPressAvatar: messageOptions.onPressAvatar, 49 | ); 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | final bool isOwnMessage = message.user.id == currentUser.id; 55 | bool isPreviousSameAuthor = false; 56 | bool isNextSameAuthor = false; 57 | if (previousMessage != null && 58 | previousMessage!.user.id == message.user.id) { 59 | isPreviousSameAuthor = true; 60 | } 61 | if (nextMessage != null && nextMessage!.user.id == message.user.id) { 62 | isNextSameAuthor = true; 63 | } 64 | 65 | return Container( 66 | margin: isAfterDateSeparator 67 | ? EdgeInsets.zero 68 | : isPreviousSameAuthor 69 | ? messageOptions.marginSameAuthor 70 | : messageOptions.marginDifferentAuthor, 71 | child: Row( 72 | crossAxisAlignment: CrossAxisAlignment.end, 73 | mainAxisAlignment: 74 | isOwnMessage ? MainAxisAlignment.end : MainAxisAlignment.start, 75 | children: [ 76 | if (messageOptions.showOtherUsersAvatar) 77 | Opacity( 78 | opacity: 79 | !isOwnMessage && (!isNextSameAuthor || isBeforeDateSeparator) 80 | ? 1 81 | : 0, 82 | child: getAvatar(), 83 | ), 84 | if (!messageOptions.showOtherUsersAvatar) 85 | SizedBox(width: messageOptions.spaceWhenAvatarIsHidden), 86 | GestureDetector( 87 | onLongPress: messageOptions.onLongPressMessage != null 88 | ? () => messageOptions.onLongPressMessage!(message) 89 | : null, 90 | onTap: messageOptions.onPressMessage != null 91 | ? () => messageOptions.onPressMessage!(message) 92 | : null, 93 | child: ConstrainedBox( 94 | constraints: BoxConstraints( 95 | maxWidth: messageOptions.maxWidth ?? 96 | MediaQuery.of(context).size.width * 0.7, 97 | ), 98 | child: Column( 99 | crossAxisAlignment: isOwnMessage 100 | ? CrossAxisAlignment.end 101 | : CrossAxisAlignment.start, 102 | mainAxisAlignment: MainAxisAlignment.end, 103 | children: [ 104 | if (messageOptions.top != null) 105 | messageOptions.top!(message, previousMessage, nextMessage), 106 | if (!isOwnMessage && 107 | messageOptions.showOtherUsersName && 108 | (!isPreviousSameAuthor || isAfterDateSeparator)) 109 | messageOptions.userNameBuilder != null 110 | ? messageOptions.userNameBuilder!(message.user) 111 | : DefaultUserName(user: message.user), 112 | if (message.medias != null && 113 | message.medias!.isNotEmpty && 114 | messageOptions.textBeforeMedia) 115 | messageOptions.messageMediaBuilder != null 116 | ? messageOptions.messageMediaBuilder!( 117 | message, previousMessage, nextMessage) 118 | : MediaContainer( 119 | message: message, 120 | isOwnMessage: isOwnMessage, 121 | messageOptions: messageOptions, 122 | ), 123 | if (message.text.isNotEmpty) 124 | TextContainer( 125 | messageOptions: messageOptions, 126 | message: message, 127 | previousMessage: previousMessage, 128 | nextMessage: nextMessage, 129 | isOwnMessage: isOwnMessage, 130 | isNextSameAuthor: isNextSameAuthor, 131 | isPreviousSameAuthor: isPreviousSameAuthor, 132 | isAfterDateSeparator: isAfterDateSeparator, 133 | isBeforeDateSeparator: isBeforeDateSeparator, 134 | messageTextBuilder: messageOptions.messageTextBuilder, 135 | ), 136 | if (message.medias != null && 137 | message.medias!.isNotEmpty && 138 | !messageOptions.textBeforeMedia) 139 | messageOptions.messageMediaBuilder != null 140 | ? messageOptions.messageMediaBuilder!( 141 | message, previousMessage, nextMessage) 142 | : MediaContainer( 143 | message: message, 144 | isOwnMessage: isOwnMessage, 145 | messageOptions: messageOptions, 146 | ), 147 | if (messageOptions.bottom != null) 148 | messageOptions.bottom!( 149 | message, previousMessage, nextMessage), 150 | ], 151 | ), 152 | ), 153 | ), 154 | if (messageOptions.showCurrentUserAvatar) 155 | Opacity( 156 | opacity: isOwnMessage && !isNextSameAuthor ? 1 : 0, 157 | child: getAvatar(), 158 | ), 159 | if (!messageOptions.showCurrentUserAvatar) 160 | SizedBox(width: messageOptions.spaceWhenAvatarIsHidden), 161 | ], 162 | ), 163 | ); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /example/lib/data.dart: -------------------------------------------------------------------------------- 1 | import 'package:dash_chat_2/dash_chat_2.dart'; 2 | 3 | String profileImage = 4 | 'https://firebasestorage.googleapis.com/v0/b/molteo-40978.appspot.com/o/1-intro-photo-final.jpeg?alt=media&token=daf78997-d8f0-49d1-9120-a9380bde48b5'; 5 | 6 | // We have all the possibilities for users 7 | ChatUser user = ChatUser(id: '0'); 8 | ChatUser user1 = ChatUser(id: '1'); 9 | ChatUser user2 = ChatUser(id: '2', firstName: 'Niki Lauda'); 10 | ChatUser user3 = ChatUser(id: '3', lastName: 'Clark'); 11 | ChatUser user4 = ChatUser(id: '4', profileImage: profileImage); 12 | ChatUser user5 = ChatUser(id: '5', firstName: 'Charles', lastName: 'Leclerc'); 13 | ChatUser user6 = 14 | ChatUser(id: '6', firstName: 'Max', profileImage: profileImage); 15 | ChatUser user7 = 16 | ChatUser(id: '7', lastName: 'Toto', profileImage: profileImage); 17 | ChatUser user8 = ChatUser( 18 | id: '8', firstName: 'Toto', lastName: 'Clark', profileImage: profileImage); 19 | 20 | List allUsersSample = [ 21 | ChatMessage( 22 | text: 'Test', 23 | user: user1, 24 | createdAt: DateTime(2021, 01, 30, 16, 45), 25 | ), 26 | ChatMessage( 27 | text: 'Test', 28 | user: user2, 29 | createdAt: DateTime(2021, 01, 30, 16, 45), 30 | ), 31 | ChatMessage( 32 | text: 'Test', 33 | user: user3, 34 | createdAt: DateTime(2021, 01, 30, 16, 45), 35 | ), 36 | ChatMessage( 37 | text: 'Test', 38 | user: user4, 39 | createdAt: DateTime(2021, 01, 30, 16, 45), 40 | ), 41 | ChatMessage( 42 | text: 'Test', 43 | user: user5, 44 | createdAt: DateTime(2021, 01, 30, 16, 45), 45 | ), 46 | ChatMessage( 47 | text: 'Test', 48 | user: user6, 49 | createdAt: DateTime(2021, 01, 30, 16, 45), 50 | ), 51 | ChatMessage( 52 | text: 'Test', 53 | user: user7, 54 | createdAt: DateTime(2021, 01, 30, 16, 45), 55 | ), 56 | ChatMessage( 57 | text: 'Test', 58 | user: user8, 59 | createdAt: DateTime(2021, 01, 30, 16, 45), 60 | ), 61 | ]; 62 | 63 | List basicSample = [ 64 | ChatMessage( 65 | text: 'google.com hello you @Marc is it &you okay?', 66 | user: user2, 67 | createdAt: DateTime(2021, 01, 31, 16, 45), 68 | mentions: [ 69 | Mention(title: '@Marc'), 70 | Mention(title: '&you'), 71 | ], 72 | ), 73 | ChatMessage( 74 | text: 'google.com', 75 | user: user2, 76 | createdAt: DateTime(2021, 01, 30, 16, 45), 77 | ), 78 | ChatMessage( 79 | text: "Oh what's up guys?", 80 | user: user2, 81 | createdAt: DateTime(2021, 01, 30, 16, 45), 82 | ), 83 | ChatMessage( 84 | text: 'How you doin?', 85 | user: user8, 86 | createdAt: DateTime(2021, 01, 30, 16, 34), 87 | ), 88 | ChatMessage( 89 | isMarkdown: true, 90 | text: 91 | "```dart\nvoid main() {\n print('Hello World');\n}\n```\nThe above code will print \"Hello World\" to the console when run.\n\nHere's a breakdown of the code:\n\n* The `main()` function is the entry point of the program. It's where execution begins.\n* `print('Hello World')` prints \"Hello World\" to the console. The `print()` function is a built-in function in Dart that outputs data to the console.\n\nYou can run this code by creating a new Dart file (e.g., `hello_world.dart`) and pasting the code into it. Then, open a terminal window, navigate to the directory where the file is saved, and run the following command:\n\n```\ndart hello_world.dart\n```\n\nThis will compile and run the Dart program, and you should see \"Hello World\" printed to the console. Know more: www.google.com ", 92 | user: user2, 93 | createdAt: DateTime(2021, 01, 30, 15, 50), 94 | ), 95 | ChatMessage( 96 | text: 'Hey!', 97 | user: user, 98 | createdAt: DateTime(2021, 01, 30, 15, 50), 99 | ), 100 | ChatMessage( 101 | text: 'Hey!', 102 | user: user, 103 | createdAt: DateTime(2021, 01, 28, 15, 50), 104 | ), 105 | ChatMessage( 106 | text: 'Hey!', 107 | user: user, 108 | createdAt: DateTime(2021, 01, 28, 15, 50), 109 | ), 110 | ]; 111 | 112 | List media = [ 113 | ChatMessage( 114 | medias: [ 115 | ChatMedia( 116 | url: 117 | 'https://firebasestorage.googleapis.com/v0/b/molteo-40978.appspot.com/o/memes%2F155512641_3864499247004975_4028017188079714246_n.jpg?alt=media&token=0b335455-93ed-4529-9055-9a2c741e0189', 118 | type: MediaType.image, 119 | fileName: 'image.png', 120 | isUploading: true, 121 | ), 122 | ChatMedia( 123 | url: 124 | 'https://firebasestorage.googleapis.com/v0/b/molteo-40978.appspot.com/o/memes%2F155512641_3864499247004975_4028017188079714246_n.jpg?alt=media&token=0b335455-93ed-4529-9055-9a2c741e0189', 125 | type: MediaType.image, 126 | fileName: 'image.png', 127 | ), 128 | ChatMedia( 129 | url: 130 | 'https://firebasestorage.googleapis.com/v0/b/molteo-40978.appspot.com/o/chat_medias%2F2GFlPkj94hKCqonpEdf1%2F20210526_162318.mp4?alt=media&token=01b814b9-d93a-4bf1-8be1-cf9a49058f97', 131 | type: MediaType.video, 132 | fileName: 'video.mp4', 133 | isUploading: false, 134 | ), 135 | ChatMedia( 136 | url: 137 | 'https://firebasestorage.googleapis.com/v0/b/molteo-40978.appspot.com/o/chat_medias%2F2GFlPkj94hKCqonpEdf1%2F20210526_162318.mp4?alt=media&token=01b814b9-d93a-4bf1-8be1-cf9a49058f97', 138 | type: MediaType.video, 139 | fileName: 'video.mp4', 140 | isUploading: false, 141 | ), 142 | ChatMedia( 143 | url: 144 | 'https://firebasestorage.googleapis.com/v0/b/molteo-40978.appspot.com/o/memes%2F155512641_3864499247004975_4028017188079714246_n.jpg?alt=media&token=0b335455-93ed-4529-9055-9a2c741e0189', 145 | type: MediaType.file, 146 | fileName: 'image.png', 147 | ), 148 | ChatMedia( 149 | url: 150 | 'https://firebasestorage.googleapis.com/v0/b/molteo-40978.appspot.com/o/memes%2F155512641_3864499247004975_4028017188079714246_n.jpg?alt=media&token=0b335455-93ed-4529-9055-9a2c741e0189', 151 | type: MediaType.image, 152 | fileName: 'image.png', 153 | ) 154 | ], 155 | user: user3, 156 | createdAt: DateTime(2021, 01, 30, 16, 34), 157 | ), 158 | ]; 159 | 160 | List quickReplies = [ 161 | ChatMessage( 162 | text: 'How you doin?', 163 | user: user3, 164 | createdAt: DateTime.now(), 165 | quickReplies: [ 166 | QuickReply(title: 'Great!'), 167 | QuickReply(title: 'Awesome'), 168 | ], 169 | ), 170 | ]; 171 | 172 | List mentionSample = [ 173 | ChatMessage( 174 | text: 'Hello @Niki, you should check #channel', 175 | user: user2, 176 | createdAt: DateTime(2021, 01, 31, 16, 45), 177 | mentions: [ 178 | Mention(title: '@Niki', customProperties: {'userId': user5.id}), 179 | Mention(title: '#channel'), 180 | ], 181 | ), 182 | ChatMessage( 183 | text: "Oh what's up guys?", 184 | user: user5, 185 | createdAt: DateTime(2021, 01, 30, 16, 45), 186 | ), 187 | ]; 188 | 189 | List d = []; 190 | -------------------------------------------------------------------------------- /lib/src/models/message_options.dart: -------------------------------------------------------------------------------- 1 | part of '../../dash_chat_2.dart'; 2 | 3 | /// {@category Customization} 4 | class MessageOptions { 5 | const MessageOptions({ 6 | this.showCurrentUserAvatar = false, 7 | this.showOtherUsersAvatar = true, 8 | this.showOtherUsersName = true, 9 | this.userNameBuilder, 10 | this.avatarBuilder, 11 | this.onPressAvatar, 12 | this.onLongPressAvatar, 13 | this.onLongPressMessage, 14 | this.onPressMessage, 15 | this.onPressMention, 16 | Color? currentUserContainerColor, 17 | Color? currentUserTextColor, 18 | this.containerColor = const Color(0xFFF5F5F5), 19 | this.textColor = Colors.black, 20 | this.messagePadding = const EdgeInsets.all(11), 21 | this.maxWidth, 22 | this.messageDecorationBuilder, 23 | this.top, 24 | this.bottom, 25 | this.messageRowBuilder, 26 | this.messageTextBuilder, 27 | this.parsePatterns, 28 | this.textBeforeMedia = true, 29 | this.onTapMedia, 30 | this.showTime = false, 31 | this.timeFormat, 32 | this.messageTimeBuilder, 33 | this.messageMediaBuilder, 34 | this.borderRadius = 18.0, 35 | Color? currentUserTimeTextColor, 36 | this.marginDifferentAuthor = const EdgeInsets.only(top: 15), 37 | this.marginSameAuthor = const EdgeInsets.only(top: 2), 38 | this.spaceWhenAvatarIsHidden = 10.0, 39 | this.timeFontSize = 10.0, 40 | this.timePadding = const EdgeInsets.only(top: 5), 41 | this.markdownStyleSheet, 42 | Color? timeTextColor, 43 | }) : _currentUserContainerColor = currentUserContainerColor, 44 | _currentUserTextColor = currentUserTextColor, 45 | _currentUserTimeTextColor = currentUserTimeTextColor, 46 | _timeTextColor = timeTextColor; 47 | 48 | /// Format of the time if [showTime] is true 49 | /// Default to: DateFormat('HH:mm') 50 | final intl.DateFormat? timeFormat; 51 | 52 | /// If you want to show the time under the text of each message 53 | final bool showTime; 54 | 55 | /// If you want to show the avatar of the current user 56 | final bool showCurrentUserAvatar; 57 | 58 | /// If you want to show the avatar of the other users 59 | final bool showOtherUsersAvatar; 60 | 61 | /// If you want to show the name of the other users above the messages 62 | /// Useful in group chats 63 | final bool showOtherUsersName; 64 | 65 | /// If you want to create your own userName widget when [showOtherUsersName] is true 66 | /// You can use DefaultUserName to only override some variables 67 | final Widget Function(ChatUser user)? userNameBuilder; 68 | 69 | /// Builder to create your own avatar 70 | /// You can use DefaultAvatar to only override some variables 71 | final Widget Function( 72 | ChatUser, Function? onPressAvatar, Function? onLongPressAvatar)? 73 | avatarBuilder; 74 | 75 | /// Function to call when the user press on an avatar 76 | final Function(ChatUser)? onPressAvatar; 77 | 78 | /// Function to call when the user long press on an avatar 79 | final Function(ChatUser)? onLongPressAvatar; 80 | 81 | /// Function to call when the user long press on a message 82 | final Function(ChatMessage)? onLongPressMessage; 83 | 84 | /// Function to call when the user press on a message 85 | final Function(ChatMessage)? onPressMessage; 86 | 87 | /// Function to call when the user press on a message mention 88 | final Function(Mention)? onPressMention; 89 | 90 | /// Color of the current user chat bubbles 91 | /// 92 | /// Default to: `Theme.of(context).primaryColor` 93 | Color currentUserContainerColor(BuildContext context) { 94 | return _currentUserContainerColor ?? Theme.of(context).primaryColor; 95 | } 96 | 97 | /// Used to calculate [currentUserContainerColor] 98 | final Color? _currentUserContainerColor; 99 | 100 | /// Color of the current user text in chat bubbles 101 | /// 102 | /// Default to: `Theme.of(context).colorScheme.onPrimary` 103 | Color currentUserTextColor(BuildContext context) { 104 | return _currentUserTextColor ?? Theme.of(context).colorScheme.onPrimary; 105 | } 106 | 107 | /// Used to calculate [currentUserTextColor] 108 | final Color? _currentUserTextColor; 109 | 110 | /// Color of current user time text in chat bubbles 111 | /// 112 | /// Default to: `currentUserTextColor` 113 | Color currentUserTimeTextColor(BuildContext context) { 114 | return _currentUserTimeTextColor ?? currentUserTextColor(context); 115 | } 116 | 117 | /// Used to calculate [currentUserTimeTextColor] 118 | final Color? _currentUserTimeTextColor; 119 | 120 | /// Color of the other users chat bubbles 121 | /// 122 | /// Default to: `Colors.grey.shade100` 123 | final Color containerColor; 124 | 125 | /// Color of the other users text in chat bubbles 126 | /// 127 | /// Default to: `Colors.black` 128 | final Color textColor; 129 | 130 | /// Color of other users time text in chat bubbles 131 | /// 132 | /// Default to: `textColor` 133 | 134 | Color timeTextColor() { 135 | return _timeTextColor ?? textColor; 136 | } 137 | 138 | /// Used to calculate [timeTextColor] 139 | final Color? _timeTextColor; 140 | 141 | /// Builder to create the entire message row yourself 142 | final Widget Function( 143 | ChatMessage message, 144 | ChatMessage? previousMessage, 145 | ChatMessage? nextMessage, 146 | bool isAfterDateSeparator, 147 | bool isBeforeDateSeparator, 148 | )? messageRowBuilder; 149 | 150 | /// Builder to create own message text widget 151 | final Widget Function(ChatMessage message, ChatMessage? previousMessage, 152 | ChatMessage? nextMessage)? messageTextBuilder; 153 | 154 | /// Builder to create your own media container widget 155 | final Widget Function(ChatMessage message, ChatMessage? previousMessage, 156 | ChatMessage? nextMessage)? messageMediaBuilder; 157 | 158 | /// Builder to create your own time widget 159 | /// (shown under the text when [showTime] is true) 160 | final Widget Function(ChatMessage message, bool isOwnMessage)? 161 | messageTimeBuilder; 162 | 163 | /// List of MatchText using flutter_parsed_text library 164 | /// to parse and customize accordingly some part of the text 165 | /// By default ParsedType.URL is set and will use launchUrl to open the link 166 | final List? parsePatterns; 167 | 168 | /// Padding around the text in chat bubbles 169 | /// 170 | /// Default to: `EdgeInsets.all(11)` 171 | final EdgeInsets messagePadding; 172 | 173 | /// Max message width 174 | /// 175 | /// Default to: `MediaQuery.of(context).size.width * 0.7` 176 | final double? maxWidth; 177 | 178 | /// When a message have both an text and a list of media 179 | /// it will determine which one th show first 180 | final bool textBeforeMedia; 181 | 182 | /// To create your own BoxDecoration fot the chat bubble 183 | /// You can use defaultMessageDecoration to only override some variables 184 | final BoxDecoration Function( 185 | ChatMessage message, 186 | ChatMessage? previousMessage, 187 | ChatMessage? nextMessage)? messageDecorationBuilder; 188 | 189 | /// A widget to show above the chat bubble 190 | final Widget Function(ChatMessage message, ChatMessage? previousMessage, 191 | ChatMessage? nextMessage)? top; 192 | 193 | /// A widget to show under the chat bubble 194 | final Widget Function(ChatMessage message, ChatMessage? previousMessage, 195 | ChatMessage? nextMessage)? bottom; 196 | 197 | /// Function to call when the user clicks on a media 198 | /// Will not work with the default video player 199 | final void Function(ChatMedia media)? onTapMedia; 200 | 201 | /// Border radius of the chat bubbles 202 | /// 203 | /// Default to: `18.0` 204 | final double borderRadius; 205 | 206 | /// Margin around the chat bubble when previous author is different 207 | /// 208 | /// Default to: `const EdgeInsets.only(top: 15)` 209 | final EdgeInsets marginDifferentAuthor; 210 | 211 | /// Margin around the chat bubble when previous author is the same 212 | /// 213 | /// Default to: `const EdgeInsets.only(top: 2)` 214 | final EdgeInsets marginSameAuthor; 215 | 216 | /// Space between chat bubble and edge of the list when avatar is hidden via [showOtherUsersAvatar] or [showCurrentUserAvatar] 217 | /// 218 | /// Default to: `10.0` 219 | final double spaceWhenAvatarIsHidden; 220 | 221 | /// Font size of the time text in chat bubbles 222 | /// 223 | /// Default to: `10.0` 224 | final double timeFontSize; 225 | 226 | /// Space between time and message text in chat bubbles 227 | /// 228 | /// Default to: `const EdgeInsets.only(top: 5)` 229 | final EdgeInsets timePadding; 230 | 231 | /// Stylesheet for markdown message rendering 232 | final MarkdownStyleSheet? markdownStyleSheet; 233 | } 234 | -------------------------------------------------------------------------------- /lib/src/widgets/input_toolbar/input_toolbar.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// @nodoc 4 | class InputToolbar extends StatefulWidget { 5 | const InputToolbar({ 6 | required this.currentUser, 7 | required this.onSend, 8 | this.inputOptions = const InputOptions(), 9 | Key? key, 10 | }) : super(key: key); 11 | 12 | /// Options to custom the toolbar 13 | final InputOptions inputOptions; 14 | 15 | /// Function to call when the message is sent (click on the send button) 16 | final Function(ChatMessage) onSend; 17 | 18 | /// Current user using the chat 19 | final ChatUser currentUser; 20 | 21 | @override 22 | State createState() => InputToolbarState(); 23 | } 24 | 25 | class InputToolbarState extends State 26 | with WidgetsBindingObserver { 27 | late TextEditingController textController; 28 | OverlayEntry? _overlayEntry; 29 | int currentMentionIndex = -1; 30 | String currentTrigger = ''; 31 | late FocusNode focusNode; 32 | 33 | @override 34 | void initState() { 35 | textController = 36 | widget.inputOptions.textController ?? TextEditingController(); 37 | focusNode = widget.inputOptions.focusNode ?? FocusNode(); 38 | focusNode.addListener(() { 39 | if (!focusNode.hasFocus) { 40 | _clearOverlay(); 41 | } 42 | }); 43 | WidgetsBinding.instance.addObserver(this); 44 | super.initState(); 45 | } 46 | 47 | @override 48 | void didChangeMetrics() { 49 | final double bottomInset = View.of(context).viewInsets.bottom; 50 | final bool isKeyboardActive = bottomInset > 0.0; 51 | if (!isKeyboardActive) { 52 | _clearOverlay(); 53 | } 54 | } 55 | 56 | @override 57 | void dispose() { 58 | WidgetsBinding.instance.removeObserver(this); 59 | _clearOverlay(); 60 | super.dispose(); 61 | } 62 | 63 | @override 64 | Widget build(BuildContext context) { 65 | return SafeArea( 66 | top: false, 67 | child: Container( 68 | padding: widget.inputOptions.inputToolbarPadding, 69 | margin: widget.inputOptions.inputToolbarMargin, 70 | decoration: widget.inputOptions.inputToolbarStyle, 71 | child: Row( 72 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 73 | crossAxisAlignment: CrossAxisAlignment.center, 74 | children: [ 75 | if (widget.inputOptions.leading != null) 76 | ...widget.inputOptions.leading!, 77 | Expanded( 78 | child: Directionality( 79 | textDirection: widget.inputOptions.inputTextDirection, 80 | child: TextField( 81 | focusNode: focusNode, 82 | controller: textController, 83 | enabled: !widget.inputOptions.inputDisabled, 84 | textCapitalization: widget.inputOptions.textCapitalization, 85 | textInputAction: widget.inputOptions.textInputAction, 86 | decoration: widget.inputOptions.inputDecoration ?? 87 | defaultInputDecoration(), 88 | maxLength: widget.inputOptions.maxInputLength, 89 | minLines: 1, 90 | maxLines: widget.inputOptions.sendOnEnter 91 | ? 1 92 | : widget.inputOptions.inputMaxLines, 93 | cursorColor: widget.inputOptions.cursorStyle.color, 94 | cursorWidth: widget.inputOptions.cursorStyle.width, 95 | showCursor: !widget.inputOptions.cursorStyle.hide, 96 | style: widget.inputOptions.inputTextStyle, 97 | onSubmitted: (String value) { 98 | if (widget.inputOptions.sendOnEnter) { 99 | _sendMessage(); 100 | } 101 | }, 102 | onChanged: (String value) async { 103 | setState(() {}); 104 | if (widget.inputOptions.onTextChange != null) { 105 | widget.inputOptions.onTextChange!(value); 106 | } 107 | WidgetsBinding.instance.addPostFrameCallback((_) async { 108 | if (widget.inputOptions.onMention != null) { 109 | await _checkMentions(value); 110 | } 111 | }); 112 | }, 113 | autocorrect: widget.inputOptions.autocorrect, 114 | ), 115 | ), 116 | ), 117 | if (widget.inputOptions.trailing != null && 118 | widget.inputOptions.showTraillingBeforeSend) 119 | ...widget.inputOptions.trailing!, 120 | if (widget.inputOptions.alwaysShowSend || 121 | textController.text.isNotEmpty) 122 | widget.inputOptions.sendButtonBuilder != null 123 | ? widget.inputOptions.sendButtonBuilder!(_sendMessage) 124 | : defaultSendButton(color: Theme.of(context).primaryColor)( 125 | _sendMessage, 126 | ), 127 | if (widget.inputOptions.trailing != null && 128 | !widget.inputOptions.showTraillingBeforeSend) 129 | ...widget.inputOptions.trailing!, 130 | ], 131 | ), 132 | ), 133 | ); 134 | } 135 | 136 | Future _checkMentions(String text) async { 137 | bool hasMatch = false; 138 | for (final String trigger in widget.inputOptions.onMentionTriggers) { 139 | final RegExp regexp = RegExp(r'(?])' + trigger + r'([^\s<>]+)$'); 140 | if (regexp.hasMatch(text)) { 141 | hasMatch = true; 142 | currentMentionIndex = textController.text.indexOf(regexp); 143 | currentTrigger = trigger; 144 | List children = await widget.inputOptions.onMention!( 145 | trigger, 146 | regexp.firstMatch(text)!.group(1)!, 147 | _onMentionClick, 148 | ); 149 | _showMentionModal(children); 150 | } 151 | } 152 | if (!hasMatch) { 153 | _clearOverlay(); 154 | } 155 | } 156 | 157 | void _onMentionClick(String value) { 158 | textController.text = textController.text.replaceRange( 159 | currentMentionIndex, 160 | textController.text.length, 161 | currentTrigger + value, 162 | ); 163 | textController.selection = TextSelection.collapsed( 164 | offset: textController.text.length, 165 | ); 166 | _clearOverlay(); 167 | } 168 | 169 | void _clearOverlay() { 170 | if (_overlayEntry != null && _overlayEntry!.mounted) { 171 | _overlayEntry?.remove(); 172 | _overlayEntry?.dispose(); 173 | } 174 | } 175 | 176 | void _showMentionModal(List children) { 177 | final OverlayState overlay = Overlay.of(context); 178 | final RenderBox renderBox = context.findRenderObject() as RenderBox; 179 | final Offset topLeftCornerOffset = renderBox.localToGlobal(Offset.zero); 180 | 181 | double bottomPosition = 182 | MediaQuery.of(context).size.height - topLeftCornerOffset.dy; 183 | if (widget.inputOptions.inputToolbarMargin != null) { 184 | bottomPosition -= widget.inputOptions.inputToolbarMargin!.top - 185 | widget.inputOptions.inputToolbarMargin!.bottom; 186 | } 187 | 188 | _clearOverlay(); 189 | 190 | _overlayEntry = OverlayEntry( 191 | builder: (BuildContext context) { 192 | return Positioned( 193 | width: renderBox.size.width, 194 | bottom: bottomPosition, 195 | child: Container( 196 | constraints: BoxConstraints( 197 | maxHeight: MediaQuery.of(context).size.height - 198 | bottomPosition - 199 | MediaQuery.of(context).padding.top - 200 | kToolbarHeight, 201 | ), 202 | decoration: BoxDecoration( 203 | color: Theme.of(context).canvasColor, 204 | border: Border( 205 | top: BorderSide( 206 | width: 0.2, 207 | color: Theme.of(context).dividerColor, 208 | ), 209 | ), 210 | ), 211 | child: Material( 212 | color: Theme.of(context).hoverColor, 213 | child: SingleChildScrollView( 214 | child: Column( 215 | children: children, 216 | ), 217 | ), 218 | ), 219 | ), 220 | ); 221 | }, 222 | ); 223 | overlay.insert(_overlayEntry!); 224 | } 225 | 226 | void _sendMessage() { 227 | if (textController.text.isNotEmpty) { 228 | final ChatMessage message = ChatMessage( 229 | text: textController.text, 230 | user: widget.currentUser, 231 | createdAt: DateTime.now(), 232 | ); 233 | widget.onSend(message); 234 | textController.text = ''; 235 | if (widget.inputOptions.onTextChange != null) { 236 | widget.inputOptions.onTextChange!(''); 237 | } 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /lib/src/widgets/message_list/message_list.dart: -------------------------------------------------------------------------------- 1 | part of '../../../dash_chat_2.dart'; 2 | 3 | /// @nodoc 4 | class MessageList extends StatefulWidget { 5 | const MessageList({ 6 | required this.currentUser, 7 | required this.messages, 8 | this.readOnly = false, 9 | this.messageOptions = const MessageOptions(), 10 | this.messageListOptions = const MessageListOptions(), 11 | this.quickReplyOptions = const QuickReplyOptions(), 12 | this.scrollToBottomOptions = const ScrollToBottomOptions(), 13 | this.typingUsers, 14 | Key? key, 15 | }) : super(key: key); 16 | 17 | /// The current user of the chat 18 | final ChatUser currentUser; 19 | 20 | /// List of messages visible in the chat 21 | final List messages; 22 | 23 | /// Whether the chat is read only, used for safe area 24 | final bool readOnly; 25 | 26 | /// Options to customize the behaviour and design of the messages 27 | final MessageOptions messageOptions; 28 | 29 | /// Options to customize the behaviour and design of the overall list of message 30 | final MessageListOptions messageListOptions; 31 | 32 | /// Options to customize the behaviour and design of the quick replies 33 | final QuickReplyOptions quickReplyOptions; 34 | 35 | /// Options to customize the behaviour and design of the scroll-to-bottom button 36 | final ScrollToBottomOptions scrollToBottomOptions; 37 | 38 | /// List of users currently typing in the chat 39 | final List? typingUsers; 40 | 41 | @override 42 | State createState() => MessageListState(); 43 | } 44 | 45 | class MessageListState extends State { 46 | bool scrollToBottomIsVisible = false; 47 | bool isLoadingMore = false; 48 | late ScrollController scrollController; 49 | 50 | @override 51 | void initState() { 52 | scrollController = 53 | widget.messageListOptions.scrollController ?? ScrollController(); 54 | scrollController.addListener(() => _onScroll()); 55 | super.initState(); 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return GestureDetector( 61 | onTap: () => FocusScope.of(context).requestFocus(FocusNode()), 62 | child: Stack( 63 | children: [ 64 | Column( 65 | crossAxisAlignment: CrossAxisAlignment.start, 66 | children: [ 67 | Expanded( 68 | child: ListView.builder( 69 | physics: widget.messageListOptions.scrollPhysics, 70 | padding: widget.readOnly ? null : EdgeInsets.zero, 71 | controller: scrollController, 72 | reverse: true, 73 | itemCount: widget.messages.length, 74 | itemBuilder: (BuildContext context, int i) { 75 | final ChatMessage? previousMessage = 76 | i < widget.messages.length - 1 77 | ? widget.messages[i + 1] 78 | : null; 79 | final ChatMessage? nextMessage = 80 | i > 0 ? widget.messages[i - 1] : null; 81 | final ChatMessage message = widget.messages[i]; 82 | final bool isAfterDateSeparator = _shouldShowDateSeparator( 83 | previousMessage, message, widget.messageListOptions); 84 | bool isBeforeDateSeparator = false; 85 | if (nextMessage != null) { 86 | isBeforeDateSeparator = _shouldShowDateSeparator( 87 | message, nextMessage, widget.messageListOptions); 88 | } 89 | return Column( 90 | children: [ 91 | if (isAfterDateSeparator) 92 | widget.messageListOptions.dateSeparatorBuilder != null 93 | ? widget.messageListOptions 94 | .dateSeparatorBuilder!(message.createdAt) 95 | : DefaultDateSeparator( 96 | date: message.createdAt, 97 | messageListOptions: widget.messageListOptions, 98 | ), 99 | if (widget.messageOptions.messageRowBuilder != 100 | null) ...[ 101 | widget.messageOptions.messageRowBuilder!( 102 | message, 103 | previousMessage, 104 | nextMessage, 105 | isAfterDateSeparator, 106 | isBeforeDateSeparator, 107 | ), 108 | ] else 109 | MessageRow( 110 | message: widget.messages[i], 111 | nextMessage: nextMessage, 112 | previousMessage: previousMessage, 113 | currentUser: widget.currentUser, 114 | isAfterDateSeparator: isAfterDateSeparator, 115 | isBeforeDateSeparator: isBeforeDateSeparator, 116 | messageOptions: widget.messageOptions, 117 | ), 118 | ], 119 | ); 120 | }, 121 | ), 122 | ), 123 | if (widget.typingUsers != null && widget.typingUsers!.isNotEmpty) 124 | ...widget.typingUsers!.map((ChatUser user) { 125 | if (widget.messageListOptions.typingBuilder != null) { 126 | return widget.messageListOptions.typingBuilder!(user); 127 | } 128 | return DefaultTypingBuilder(user: user); 129 | }), 130 | if (widget.messageListOptions.showFooterBeforeQuickReplies && 131 | widget.messageListOptions.chatFooterBuilder != null) 132 | widget.messageListOptions.chatFooterBuilder!, 133 | if (widget.messages.isNotEmpty && 134 | widget.messages.first.quickReplies != null && 135 | widget.messages.first.quickReplies!.isNotEmpty && 136 | widget.messages.first.user.id != widget.currentUser.id) 137 | QuickReplies( 138 | quickReplies: widget.messages.first.quickReplies!, 139 | quickReplyOptions: widget.quickReplyOptions, 140 | ), 141 | if (!widget.messageListOptions.showFooterBeforeQuickReplies && 142 | widget.messageListOptions.chatFooterBuilder != null) 143 | widget.messageListOptions.chatFooterBuilder!, 144 | ], 145 | ), 146 | if (isLoadingMore) 147 | Positioned( 148 | top: 8.0, 149 | right: 0, 150 | left: 0, 151 | child: widget.messageListOptions.loadEarlierBuilder ?? 152 | const Center( 153 | child: SizedBox( 154 | child: CircularProgressIndicator(), 155 | ), 156 | ), 157 | ), 158 | if (!widget.scrollToBottomOptions.disabled && scrollToBottomIsVisible) 159 | widget.scrollToBottomOptions.scrollToBottomBuilder != null 160 | ? widget.scrollToBottomOptions 161 | .scrollToBottomBuilder!(scrollController) 162 | : DefaultScrollToBottom( 163 | scrollController: scrollController, 164 | readOnly: widget.readOnly, 165 | backgroundColor: Theme.of(context).scaffoldBackgroundColor, 166 | textColor: Theme.of(context).primaryColor, 167 | ), 168 | ], 169 | ), 170 | ); 171 | } 172 | 173 | /// Check if a date separator needs to be shown 174 | bool _shouldShowDateSeparator(ChatMessage? previousMessage, 175 | ChatMessage message, MessageListOptions messageListOptions) { 176 | if (!messageListOptions.showDateSeparator) { 177 | return false; 178 | } 179 | if (previousMessage == null) { 180 | // Means this is the first message 181 | return true; 182 | } 183 | switch (messageListOptions.separatorFrequency) { 184 | case SeparatorFrequency.days: 185 | final DateTime previousDate = DateTime( 186 | previousMessage.createdAt.year, 187 | previousMessage.createdAt.month, 188 | previousMessage.createdAt.day, 189 | ); 190 | final DateTime messageDate = DateTime( 191 | message.createdAt.year, 192 | message.createdAt.month, 193 | message.createdAt.day, 194 | ); 195 | return previousDate.difference(messageDate).inDays.abs() > 0; 196 | case SeparatorFrequency.hours: 197 | final DateTime previousDate = DateTime( 198 | previousMessage.createdAt.year, 199 | previousMessage.createdAt.month, 200 | previousMessage.createdAt.day, 201 | previousMessage.createdAt.hour, 202 | ); 203 | final DateTime messageDate = DateTime( 204 | message.createdAt.year, 205 | message.createdAt.month, 206 | message.createdAt.day, 207 | message.createdAt.hour, 208 | ); 209 | return previousDate.difference(messageDate).inHours.abs() > 0; 210 | default: 211 | return false; 212 | } 213 | } 214 | 215 | /// Scroll listener to trigger different actions: 216 | /// show scroll-to-bottom btn and LoadEarlier behaviour 217 | Future _onScroll() async { 218 | bool topReached = 219 | scrollController.offset >= scrollController.position.maxScrollExtent && 220 | !scrollController.position.outOfRange; 221 | if (topReached && 222 | widget.messageListOptions.onLoadEarlier != null && 223 | !isLoadingMore) { 224 | setState(() { 225 | isLoadingMore = true; 226 | }); 227 | showScrollToBottom(); 228 | await widget.messageListOptions.onLoadEarlier!(); 229 | setState(() { 230 | isLoadingMore = false; 231 | }); 232 | } else if (scrollController.offset > 200) { 233 | showScrollToBottom(); 234 | } else { 235 | hideScrollToBottom(); 236 | } 237 | } 238 | 239 | void showScrollToBottom() { 240 | if (!scrollToBottomIsVisible) { 241 | setState(() { 242 | scrollToBottomIsVisible = true; 243 | }); 244 | } 245 | } 246 | 247 | void hideScrollToBottom() { 248 | if (scrollToBottomIsVisible) { 249 | setState(() { 250 | scrollToBottomIsVisible = false; 251 | }); 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

The most complete Chat UI for flutter

4 |

5 | Easy to use, highly customizable and fully featured 6 |

7 |

8 | 9 |

10 | 11 | License 12 | 13 | 14 | Pub version 15 | 16 | 17 | Contributors 18 | 19 |

20 | 21 |

22 | 23 | 24 |

25 | 26 |

27 | 28 | ## Features 29 | 30 | - Fully customizable components 31 | - Multi-line TextInput 32 | - Touchable links using [flutter_parsed_text](https://pub.dev/packages/flutter_parsed_text) 33 | - Markdown support using [flutter_markdown](https://pub.dev/packages/flutter_markdown) 34 | - Avatar as user's initials or profile picture 35 | - Quick Reply messages 36 | - Load earlier messages 37 | - Scroll to bottom Widget 38 | - Multiple media support (Audio support: WIP) 39 | - @ Mention users (or anything else) 40 | - Typing users 41 | - Reply to messages - WIP 42 | - Message status - WIP 43 | 44 | You need another feature? you can use the `customProperties` field of the models, it allows you to pass other data to the library that you can then use inside custom builders to implement any feature you need. 45 | 46 | Of course, if you think this feature can be useful to other people, feel free to open an issue/pull-request to discuss including it "natively" in the package. 47 | 48 | ## Basic Usage 49 | 50 | ```dart 51 | import 'package:dash_chat_2/dash_chat_2.dart'; 52 | import 'package:flutter/material.dart'; 53 | 54 | class Basic extends StatefulWidget { 55 | @override 56 | _BasicState createState() => _BasicState(); 57 | } 58 | 59 | class _BasicState extends State { 60 | ChatUser user = ChatUser( 61 | id: '1', 62 | firstName: 'Charles', 63 | lastName: 'Leclerc', 64 | ); 65 | 66 | List messages = [ 67 | ChatMessage( 68 | text: 'Hey!', 69 | user: user, 70 | createdAt: DateTime.now(), 71 | ), 72 | ]; 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return Scaffold( 77 | appBar: AppBar( 78 | title: const Text('Basic example'), 79 | ), 80 | body: DashChat( 81 | currentUser: user, 82 | onSend: (ChatMessage m) { 83 | setState(() { 84 | messages.insert(0, m); 85 | }); 86 | }, 87 | messages: messages, 88 | ), 89 | ); 90 | } 91 | } 92 | ``` 93 | 94 | You can run the [example](example) project to see more complex ways of using the package 95 | 96 | ## Parameters of DashChat 97 | 98 | - ChatUser currentUser - required: Basically "you", we need to know who is the current user to put their messages to right side 99 | 100 | - Function(ChatMessage message) onSend - required: Function to call when the user sends a message, that's where you handle the logic to send the message to your backend and append the list of `messages` 101 | 102 | - List<ChatMessage> messages - required: The list of messages of the channel, you would usually not load all the messages at once but use the `onLoadEarlier` param of `MessageListOptions` to trigger a lazy loading 103 | 104 | - InputOptions inputOptions - optional: Options to customize the behaviour and design of the chat input 105 | 106 | - MessageOptions messageOptions - optional: Options to customize the behaviour and design of the messages 107 | 108 | - MessageListOptions messageListOptions - optional: Options to customize the behaviour and design of the overall list of message 109 | 110 | - QuickReplyOptions quickReplyOptions - optional: Options to customize the behaviour and design of the quick replies 111 | 112 | - ScrollToBottomOptions scrollToBottomOptions - optional: Options to customize the behaviour and design of the scroll-to-bottom button 113 | 114 | - readOnly - optional (default to false): Option to make the chat read only, it will hide the input field 115 | 116 | - List<ChatUser> typingUsers - optional: List of users currently typing in the chat 117 | 118 | ## Full documentation 119 | 120 | You can browse the full Dart documentation here: [Documentation](https://pub.dev/documentation/dash_chat_2/latest/) 121 | 122 | ## Found this project useful? 123 | 124 | If you found this project useful, then please consider giving it a ⭐️ on Github: [https://github.com/SebastienBtr/Dash-Chat-2](https://github.com/SebastienBtr/Dash-Chat-2) 125 | 126 | ## Issues and feedback 127 | 128 | If you have any suggestions for including a feature or if something doesn't work, feel free to open a Github [issue](https://github.com/SebastienBtr/Dash-Chat-2/issues) or to open a [pull request](https://github.com/SebastienBtr/Dash-Chat-2/pulls), you are more than welcome to contribute! 129 | 130 | ## Contributors 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 |
SebastienBtr
SebastienBtr

💻 🎨
chuusungmin
chuusungmin

💻
fufesou
fufesou

💻
Nwachi ifeanyichukwu Victor
Nwachi ifeanyichukwu Victor

💻
Kaede Games
Kaede Games

💻
Derek Pitts
Derek Pitts

💻
Alex Fernandez
Alex Fernandez

💻
lawrence
lawrence

💻
Md. Al-Amin
Md. Al-Amin

💻
TPF
TPF

💻
153 | 154 | 155 | 156 | 157 | 158 | 159 | ## Credits 160 | 161 | Thanks to [Fayeed](https://github.com/fayeed) who created the v1 of this package: [https://github.com/fayeed/dash_chat](https://github.com/fayeed/dash_chat) and made that possible! 162 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | args: 5 | dependency: transitive 6 | description: 7 | name: args 8 | sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.5.0" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.11.0" 20 | boolean_selector: 21 | dependency: transitive 22 | description: 23 | name: boolean_selector 24 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.1.1" 28 | cached_network_image: 29 | dependency: "direct main" 30 | description: 31 | name: cached_network_image 32 | sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "3.3.1" 36 | cached_network_image_platform_interface: 37 | dependency: transitive 38 | description: 39 | name: cached_network_image_platform_interface 40 | sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "4.0.0" 44 | cached_network_image_web: 45 | dependency: transitive 46 | description: 47 | name: cached_network_image_web 48 | sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.2.0" 52 | characters: 53 | dependency: transitive 54 | description: 55 | name: characters 56 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.0" 60 | clock: 61 | dependency: transitive 62 | description: 63 | name: clock 64 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.1.1" 68 | collection: 69 | dependency: transitive 70 | description: 71 | name: collection 72 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.18.0" 76 | crypto: 77 | dependency: transitive 78 | description: 79 | name: crypto 80 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "3.0.3" 84 | csslib: 85 | dependency: transitive 86 | description: 87 | name: csslib 88 | sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "1.0.0" 92 | fake_async: 93 | dependency: transitive 94 | description: 95 | name: fake_async 96 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "1.3.1" 100 | ffi: 101 | dependency: transitive 102 | description: 103 | name: ffi 104 | sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "2.1.2" 108 | file: 109 | dependency: transitive 110 | description: 111 | name: file 112 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "7.0.0" 116 | fixnum: 117 | dependency: transitive 118 | description: 119 | name: fixnum 120 | sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "1.1.0" 124 | flutter: 125 | dependency: "direct main" 126 | description: flutter 127 | source: sdk 128 | version: "0.0.0" 129 | flutter_cache_manager: 130 | dependency: transitive 131 | description: 132 | name: flutter_cache_manager 133 | sha256: "395d6b7831f21f3b989ebedbb785545932adb9afe2622c1ffacf7f4b53a7e544" 134 | url: "https://pub.dev" 135 | source: hosted 136 | version: "3.3.2" 137 | flutter_lints: 138 | dependency: "direct dev" 139 | description: 140 | name: flutter_lints 141 | sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" 142 | url: "https://pub.dev" 143 | source: hosted 144 | version: "3.0.2" 145 | flutter_markdown: 146 | dependency: "direct main" 147 | description: 148 | name: flutter_markdown 149 | sha256: "9921f9deda326f8a885e202b1e35237eadfc1345239a0f6f0f1ff287e047547f" 150 | url: "https://pub.dev" 151 | source: hosted 152 | version: "0.7.1" 153 | flutter_parsed_text: 154 | dependency: "direct main" 155 | description: 156 | name: flutter_parsed_text 157 | sha256: "529cf5793b7acdf16ee0f97b158d0d4ba0bf06e7121ef180abe1a5b59e32c1e2" 158 | url: "https://pub.dev" 159 | source: hosted 160 | version: "2.2.1" 161 | flutter_test: 162 | dependency: "direct dev" 163 | description: flutter 164 | source: sdk 165 | version: "0.0.0" 166 | flutter_web_plugins: 167 | dependency: transitive 168 | description: flutter 169 | source: sdk 170 | version: "0.0.0" 171 | html: 172 | dependency: transitive 173 | description: 174 | name: html 175 | sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "0.15.4" 179 | http: 180 | dependency: transitive 181 | description: 182 | name: http 183 | sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "1.2.1" 187 | http_parser: 188 | dependency: transitive 189 | description: 190 | name: http_parser 191 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 192 | url: "https://pub.dev" 193 | source: hosted 194 | version: "4.0.2" 195 | intl: 196 | dependency: "direct main" 197 | description: 198 | name: intl 199 | sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf 200 | url: "https://pub.dev" 201 | source: hosted 202 | version: "0.19.0" 203 | leak_tracker: 204 | dependency: transitive 205 | description: 206 | name: leak_tracker 207 | sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" 208 | url: "https://pub.dev" 209 | source: hosted 210 | version: "10.0.0" 211 | leak_tracker_flutter_testing: 212 | dependency: transitive 213 | description: 214 | name: leak_tracker_flutter_testing 215 | sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 216 | url: "https://pub.dev" 217 | source: hosted 218 | version: "2.0.1" 219 | leak_tracker_testing: 220 | dependency: transitive 221 | description: 222 | name: leak_tracker_testing 223 | sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 224 | url: "https://pub.dev" 225 | source: hosted 226 | version: "2.0.1" 227 | lints: 228 | dependency: transitive 229 | description: 230 | name: lints 231 | sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 232 | url: "https://pub.dev" 233 | source: hosted 234 | version: "3.0.0" 235 | markdown: 236 | dependency: transitive 237 | description: 238 | name: markdown 239 | sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 240 | url: "https://pub.dev" 241 | source: hosted 242 | version: "7.2.2" 243 | matcher: 244 | dependency: transitive 245 | description: 246 | name: matcher 247 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 248 | url: "https://pub.dev" 249 | source: hosted 250 | version: "0.12.16+1" 251 | material_color_utilities: 252 | dependency: transitive 253 | description: 254 | name: material_color_utilities 255 | sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" 256 | url: "https://pub.dev" 257 | source: hosted 258 | version: "0.8.0" 259 | meta: 260 | dependency: transitive 261 | description: 262 | name: meta 263 | sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 264 | url: "https://pub.dev" 265 | source: hosted 266 | version: "1.11.0" 267 | octo_image: 268 | dependency: transitive 269 | description: 270 | name: octo_image 271 | sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" 272 | url: "https://pub.dev" 273 | source: hosted 274 | version: "2.0.0" 275 | path: 276 | dependency: transitive 277 | description: 278 | name: path 279 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 280 | url: "https://pub.dev" 281 | source: hosted 282 | version: "1.9.0" 283 | path_provider: 284 | dependency: transitive 285 | description: 286 | name: path_provider 287 | sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 288 | url: "https://pub.dev" 289 | source: hosted 290 | version: "2.1.3" 291 | path_provider_android: 292 | dependency: transitive 293 | description: 294 | name: path_provider_android 295 | sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d 296 | url: "https://pub.dev" 297 | source: hosted 298 | version: "2.2.4" 299 | path_provider_foundation: 300 | dependency: transitive 301 | description: 302 | name: path_provider_foundation 303 | sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 304 | url: "https://pub.dev" 305 | source: hosted 306 | version: "2.4.0" 307 | path_provider_linux: 308 | dependency: transitive 309 | description: 310 | name: path_provider_linux 311 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 312 | url: "https://pub.dev" 313 | source: hosted 314 | version: "2.2.1" 315 | path_provider_platform_interface: 316 | dependency: transitive 317 | description: 318 | name: path_provider_platform_interface 319 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 320 | url: "https://pub.dev" 321 | source: hosted 322 | version: "2.1.2" 323 | path_provider_windows: 324 | dependency: transitive 325 | description: 326 | name: path_provider_windows 327 | sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" 328 | url: "https://pub.dev" 329 | source: hosted 330 | version: "2.2.1" 331 | platform: 332 | dependency: transitive 333 | description: 334 | name: platform 335 | sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" 336 | url: "https://pub.dev" 337 | source: hosted 338 | version: "3.1.4" 339 | plugin_platform_interface: 340 | dependency: transitive 341 | description: 342 | name: plugin_platform_interface 343 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 344 | url: "https://pub.dev" 345 | source: hosted 346 | version: "2.1.8" 347 | rxdart: 348 | dependency: transitive 349 | description: 350 | name: rxdart 351 | sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" 352 | url: "https://pub.dev" 353 | source: hosted 354 | version: "0.27.7" 355 | sky_engine: 356 | dependency: transitive 357 | description: flutter 358 | source: sdk 359 | version: "0.0.99" 360 | source_span: 361 | dependency: transitive 362 | description: 363 | name: source_span 364 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 365 | url: "https://pub.dev" 366 | source: hosted 367 | version: "1.10.0" 368 | sprintf: 369 | dependency: transitive 370 | description: 371 | name: sprintf 372 | sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" 373 | url: "https://pub.dev" 374 | source: hosted 375 | version: "7.0.0" 376 | sqflite: 377 | dependency: transitive 378 | description: 379 | name: sqflite 380 | sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d 381 | url: "https://pub.dev" 382 | source: hosted 383 | version: "2.3.3+1" 384 | sqflite_common: 385 | dependency: transitive 386 | description: 387 | name: sqflite_common 388 | sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" 389 | url: "https://pub.dev" 390 | source: hosted 391 | version: "2.5.4" 392 | stack_trace: 393 | dependency: transitive 394 | description: 395 | name: stack_trace 396 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 397 | url: "https://pub.dev" 398 | source: hosted 399 | version: "1.11.1" 400 | stream_channel: 401 | dependency: transitive 402 | description: 403 | name: stream_channel 404 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 405 | url: "https://pub.dev" 406 | source: hosted 407 | version: "2.1.2" 408 | string_scanner: 409 | dependency: transitive 410 | description: 411 | name: string_scanner 412 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 413 | url: "https://pub.dev" 414 | source: hosted 415 | version: "1.2.0" 416 | synchronized: 417 | dependency: transitive 418 | description: 419 | name: synchronized 420 | sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" 421 | url: "https://pub.dev" 422 | source: hosted 423 | version: "3.1.0+1" 424 | term_glyph: 425 | dependency: transitive 426 | description: 427 | name: term_glyph 428 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 429 | url: "https://pub.dev" 430 | source: hosted 431 | version: "1.2.1" 432 | test_api: 433 | dependency: transitive 434 | description: 435 | name: test_api 436 | sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" 437 | url: "https://pub.dev" 438 | source: hosted 439 | version: "0.6.1" 440 | typed_data: 441 | dependency: transitive 442 | description: 443 | name: typed_data 444 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 445 | url: "https://pub.dev" 446 | source: hosted 447 | version: "1.3.2" 448 | url_launcher: 449 | dependency: "direct main" 450 | description: 451 | name: url_launcher 452 | sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" 453 | url: "https://pub.dev" 454 | source: hosted 455 | version: "6.2.6" 456 | url_launcher_android: 457 | dependency: transitive 458 | description: 459 | name: url_launcher_android 460 | sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" 461 | url: "https://pub.dev" 462 | source: hosted 463 | version: "6.3.2" 464 | url_launcher_ios: 465 | dependency: transitive 466 | description: 467 | name: url_launcher_ios 468 | sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" 469 | url: "https://pub.dev" 470 | source: hosted 471 | version: "6.3.0" 472 | url_launcher_linux: 473 | dependency: transitive 474 | description: 475 | name: url_launcher_linux 476 | sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 477 | url: "https://pub.dev" 478 | source: hosted 479 | version: "3.1.1" 480 | url_launcher_macos: 481 | dependency: transitive 482 | description: 483 | name: url_launcher_macos 484 | sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" 485 | url: "https://pub.dev" 486 | source: hosted 487 | version: "3.2.0" 488 | url_launcher_platform_interface: 489 | dependency: transitive 490 | description: 491 | name: url_launcher_platform_interface 492 | sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" 493 | url: "https://pub.dev" 494 | source: hosted 495 | version: "2.3.2" 496 | url_launcher_web: 497 | dependency: transitive 498 | description: 499 | name: url_launcher_web 500 | sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" 501 | url: "https://pub.dev" 502 | source: hosted 503 | version: "2.3.1" 504 | url_launcher_windows: 505 | dependency: transitive 506 | description: 507 | name: url_launcher_windows 508 | sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 509 | url: "https://pub.dev" 510 | source: hosted 511 | version: "3.1.1" 512 | uuid: 513 | dependency: transitive 514 | description: 515 | name: uuid 516 | sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" 517 | url: "https://pub.dev" 518 | source: hosted 519 | version: "4.4.0" 520 | vector_math: 521 | dependency: transitive 522 | description: 523 | name: vector_math 524 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 525 | url: "https://pub.dev" 526 | source: hosted 527 | version: "2.1.4" 528 | video_player: 529 | dependency: "direct main" 530 | description: 531 | name: video_player 532 | sha256: db6a72d8f4fd155d0189845678f55ad2fd54b02c10dcafd11c068dbb631286c0 533 | url: "https://pub.dev" 534 | source: hosted 535 | version: "2.8.6" 536 | video_player_android: 537 | dependency: transitive 538 | description: 539 | name: video_player_android 540 | sha256: "134e1ad410d67e18a19486ed9512c72dfc6d8ffb284d0e8f2e99e903d1ba8fa3" 541 | url: "https://pub.dev" 542 | source: hosted 543 | version: "2.4.14" 544 | video_player_avfoundation: 545 | dependency: transitive 546 | description: 547 | name: video_player_avfoundation 548 | sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c 549 | url: "https://pub.dev" 550 | source: hosted 551 | version: "2.6.1" 552 | video_player_platform_interface: 553 | dependency: transitive 554 | description: 555 | name: video_player_platform_interface 556 | sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" 557 | url: "https://pub.dev" 558 | source: hosted 559 | version: "6.2.2" 560 | video_player_web: 561 | dependency: transitive 562 | description: 563 | name: video_player_web 564 | sha256: ff4d69a6614b03f055397c27a71c9d3ddea2b2a23d71b2ba0164f59ca32b8fe2 565 | url: "https://pub.dev" 566 | source: hosted 567 | version: "2.3.1" 568 | vm_service: 569 | dependency: transitive 570 | description: 571 | name: vm_service 572 | sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 573 | url: "https://pub.dev" 574 | source: hosted 575 | version: "13.0.0" 576 | web: 577 | dependency: transitive 578 | description: 579 | name: web 580 | sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" 581 | url: "https://pub.dev" 582 | source: hosted 583 | version: "0.5.1" 584 | win32: 585 | dependency: transitive 586 | description: 587 | name: win32 588 | sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" 589 | url: "https://pub.dev" 590 | source: hosted 591 | version: "5.5.0" 592 | xdg_directories: 593 | dependency: transitive 594 | description: 595 | name: xdg_directories 596 | sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d 597 | url: "https://pub.dev" 598 | source: hosted 599 | version: "1.0.4" 600 | sdks: 601 | dart: ">=3.3.0 <4.0.0" 602 | flutter: ">=3.19.0" 603 | --------------------------------------------------------------------------------