diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..5d19f763
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,253 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "flutter_chat_ui",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flyer_chat",
+ "cwd": "examples/flyer_chat",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flyer_chat (profile mode)",
+ "cwd": "examples/flyer_chat",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "flyer_chat (release mode)",
+ "cwd": "examples/flyer_chat",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ {
+ "name": "cross_cache",
+ "cwd": "packages/cross_cache",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "cross_cache (profile mode)",
+ "cwd": "packages/cross_cache",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "cross_cache (release mode)",
+ "cwd": "packages/cross_cache",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ {
+ "name": "flutter_chat_core",
+ "cwd": "packages/flutter_chat_core",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flutter_chat_core (profile mode)",
+ "cwd": "packages/flutter_chat_core",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "flutter_chat_core (release mode)",
+ "cwd": "packages/flutter_chat_core",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ {
+ "name": "flutter_chat_ui",
+ "cwd": "packages/flutter_chat_ui",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flutter_chat_ui (profile mode)",
+ "cwd": "packages/flutter_chat_ui",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "flutter_chat_ui (release mode)",
+ "cwd": "packages/flutter_chat_ui",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ {
+ "name": "flyer_chat_audio_message",
+ "cwd": "packages/flyer_chat_audio_message",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flyer_chat_audio_message (profile mode)",
+ "cwd": "packages/flyer_chat_audio_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "flyer_chat_audio_message (release mode)",
+ "cwd": "packages/flyer_chat_audio_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ {
+ "name": "flyer_chat_custom_message",
+ "cwd": "packages/flyer_chat_custom_message",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flyer_chat_custom_message (profile mode)",
+ "cwd": "packages/flyer_chat_custom_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "flyer_chat_custom_message (release mode)",
+ "cwd": "packages/flyer_chat_custom_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ {
+ "name": "flyer_chat_file_message",
+ "cwd": "packages/flyer_chat_file_message",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flyer_chat_file_message (profile mode)",
+ "cwd": "packages/flyer_chat_file_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "flyer_chat_file_message (release mode)",
+ "cwd": "packages/flyer_chat_file_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ {
+ "name": "flyer_chat_image_message",
+ "cwd": "packages/flyer_chat_image_message",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flyer_chat_image_message (profile mode)",
+ "cwd": "packages/flyer_chat_image_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "flyer_chat_image_message (release mode)",
+ "cwd": "packages/flyer_chat_image_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ {
+ "name": "flyer_chat_location_message",
+ "cwd": "packages/flyer_chat_location_message",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flyer_chat_location_message (profile mode)",
+ "cwd": "packages/flyer_chat_location_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "flyer_chat_location_message (release mode)",
+ "cwd": "packages/flyer_chat_location_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ {
+ "name": "flyer_chat_system_message",
+ "cwd": "packages/flyer_chat_system_message",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flyer_chat_system_message (profile mode)",
+ "cwd": "packages/flyer_chat_system_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "flyer_chat_system_message (release mode)",
+ "cwd": "packages/flyer_chat_system_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ {
+ "name": "flyer_chat_text_message",
+ "cwd": "packages/flyer_chat_text_message",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flyer_chat_text_message (profile mode)",
+ "cwd": "packages/flyer_chat_text_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "flyer_chat_text_message (release mode)",
+ "cwd": "packages/flyer_chat_text_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ },
+ {
+ "name": "flyer_chat_video_message",
+ "cwd": "packages/flyer_chat_video_message",
+ "request": "launch",
+ "type": "dart"
+ },
+ {
+ "name": "flyer_chat_video_message (profile mode)",
+ "cwd": "packages/flyer_chat_video_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "profile"
+ },
+ {
+ "name": "flyer_chat_video_message (release mode)",
+ "cwd": "packages/flyer_chat_video_message",
+ "request": "launch",
+ "type": "dart",
+ "flutterMode": "release"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 996e8b98..6daf67b4 100644
--- a/README.md
+++ b/README.md
@@ -9,3 +9,47 @@ Welcome to the next generation of Flutter Chat UI! ✨
💡 **Looking for the stable version?**
The v1 release is available on the [v1 branch](https://github.com/flyerhq/flutter_chat_ui/tree/v1).
+
+## Platform-Specific Configuration for Audio Recording
+
+To enable audio recording functionality, you need to configure permissions for both Android and iOS platforms.
+
+### Android
+
+1. **Add Permissions to `AndroidManifest.xml`:**
+
+ Add the following permissions to your `AndroidManifest.xml` file to allow microphone access and file writing:
+
+ ```xml
+
+
+
+ ```
+
+2. **Add Queries for Android 11 and above:**
+
+ If your app targets Android 11 (API level 30) or higher, add the following queries to allow access to certain actions:
+
+ ```xml
+
+
+
+
+
+
+ ```
+
+### iOS
+
+1. **Add Permissions to `Info.plist`:**
+
+ Add the following keys to your `Info.plist` file to request microphone access:
+
+ ```xml
+ NSMicrophoneUsageDescription
+ This app requires access to the microphone to record audio.
+ NSPhotoLibraryAddUsageDescription
+ This app requires access to save audio files.
+ ```
+
+By following these steps, you can ensure that your app has the necessary permissions to record audio on both Android and iOS platforms.
diff --git a/examples/flyer_chat/android/app/src/debug/AndroidManifest.xml b/examples/flyer_chat/android/app/src/debug/AndroidManifest.xml
index 399f6981..ffa6aff6 100644
--- a/examples/flyer_chat/android/app/src/debug/AndroidManifest.xml
+++ b/examples/flyer_chat/android/app/src/debug/AndroidManifest.xml
@@ -4,4 +4,10 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
+
+
+
+
+
+
diff --git a/examples/flyer_chat/android/app/src/main/AndroidManifest.xml b/examples/flyer_chat/android/app/src/main/AndroidManifest.xml
index c92e2ead..ef5423fc 100644
--- a/examples/flyer_chat/android/app/src/main/AndroidManifest.xml
+++ b/examples/flyer_chat/android/app/src/main/AndroidManifest.xml
@@ -1,4 +1,13 @@
+
+
+
+
+
+
+
+
+
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ NSMicrophoneUsageDescription
+ Some message to describe why you need this permission
diff --git a/examples/flyer_chat/lib/api.dart b/examples/flyer_chat/lib/api.dart
index 2b834433..863f4baa 100644
--- a/examples/flyer_chat/lib/api.dart
+++ b/examples/flyer_chat/lib/api.dart
@@ -58,10 +58,8 @@ class ApiState extends State {
return Scaffold(
body: Chat(
builders: Builders(
- textMessageBuilder: (context, message) =>
- FlyerChatTextMessage(message: message),
- imageMessageBuilder: (context, message) =>
- FlyerChatImageMessage(message: message),
+ textMessageBuilder: (context, message) => FlyerChatTextMessage(message: message),
+ imageMessageBuilder: (context, message) => FlyerChatImageMessage(message: message),
),
chatController: _chatController,
user: widget.author,
@@ -145,7 +143,7 @@ class ApiState extends State {
});
}
- void _addItem(String? text) async {
+ Future _addItem(String? text) async {
final message = await createMessage(widget.author, widget.dio, text: text);
if (mounted) {
@@ -159,8 +157,6 @@ class ApiState extends State {
);
if (mounted) {
- // Make sure to get the updated message
- // (width and height might have been set by the image message widget)
final possiblyUpdatedMessage = _chatController.messages.firstWhere(
(element) => element.id == message.id,
orElse: () => message,
diff --git a/examples/flyer_chat/lib/create_message.dart b/examples/flyer_chat/lib/create_message.dart
index a8651740..a2184b51 100644
--- a/examples/flyer_chat/lib/create_message.dart
+++ b/examples/flyer_chat/lib/create_message.dart
@@ -1,3 +1,4 @@
+import 'dart:io';
import 'dart:math';
import 'package:dio/dio.dart';
@@ -5,67 +6,71 @@ import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:flutter_lorem/flutter_lorem.dart';
import 'package:uuid/uuid.dart';
+enum MessageType { text, image, audio }
+
Future createMessage(
User author,
Dio dio, {
- bool? textOnly,
+ MessageType type = MessageType.text,
String? text,
+ File? file,
}) async {
const uuid = Uuid();
- Message message;
- if (Random().nextBool() || textOnly == true || text != null) {
- message = TextMessage(
- id: uuid.v4(),
- author: author,
- createdAt: DateTime.now().toUtc(),
- text: text ?? lorem(paragraphs: 1, words: Random().nextInt(30) + 1),
- );
- } else {
- final orientation = ['portrait', 'square', 'wide'][Random().nextInt(3)];
- late double width, height;
+ switch (type) {
+ case MessageType.text:
+ return TextMessage(
+ id: uuid.v4(),
+ author: author,
+ createdAt: DateTime.now().toUtc(),
+ text: text ?? lorem(paragraphs: 1, words: Random().nextInt(30) + 1),
+ );
+ case MessageType.image:
+ final orientation = ['portrait', 'square', 'wide'][Random().nextInt(3)];
+ late double width, height;
- if (orientation == 'portrait') {
- width = 200;
- height = 400;
- } else if (orientation == 'square') {
- width = 200;
- height = 200;
- } else {
- width = 400;
- height = 200;
- }
+ if (orientation == 'portrait') {
+ width = 200;
+ height = 400;
+ } else if (orientation == 'square') {
+ width = 200;
+ height = 200;
+ } else {
+ width = 400;
+ height = 200;
+ }
- final response = await dio.get(
- 'https://whatever.diamanthq.dev/image?w=${width.toInt()}&h=${height.toInt()}&seed=${Random().nextInt(501)}',
- options: Options(
- headers: {
- 'Access-Control-Allow-Origin': '*',
- 'Content-Type': 'application/json',
- 'Accept': '*/*',
- },
- ),
- );
+ final response = await dio.get(
+ 'https://whatever.diamanthq.dev/image?w=${width.toInt()}&h=${height.toInt()}&seed=${Random().nextInt(501)}',
+ options: Options(
+ headers: {
+ 'Access-Control-Allow-Origin': '*',
+ 'Content-Type': 'application/json',
+ 'Accept': '*/*',
+ },
+ ),
+ );
- message = ImageMessage(
- id: uuid.v4(),
- author: author,
- createdAt: DateTime.now().toUtc(),
- source: response.data['img'],
- thumbhash: response.data['thumbhash'],
- blurhash: response.data['blurhash'],
- );
+ return ImageMessage(
+ id: uuid.v4(),
+ author: author,
+ createdAt: DateTime.now().toUtc(),
+ source: response.data['img'],
+ thumbhash: response.data['thumbhash'],
+ blurhash: response.data['blurhash'],
+ );
+ case MessageType.audio:
+ if (file != null) {
+ return AudioMessage(
+ id: uuid.v4(),
+ author: author,
+ createdAt: DateTime.now().toUtc(),
+ audioFile: file.path,
+ );
+ } else {
+ throw ArgumentError('File must be provided for audio messages');
+ }
+ default:
+ throw ArgumentError('Invalid message type: $type');
}
-
- // return ImageMessage(
- // id: uuid.v4(),
- // author: author,
- // createdAt: DateTime.now().toUtc(),
- // source:
- // 'https://www.hdcarwallpapers.com/walls/audi_r8_spyder_v10_performance_rwd_2021_4k_8k-HD.jpg',
- // thumbhash: '2gcODIKwdmg9eId1l4qTb2v4xw',
- // blurhash: 'LPFFjU00^+IV~W4n%LRkROM|WBxu',
- // );
-
- return message;
}
diff --git a/examples/flyer_chat/lib/gemini.dart b/examples/flyer_chat/lib/gemini.dart
index 95c12920..e555cc73 100644
--- a/examples/flyer_chat/lib/gemini.dart
+++ b/examples/flyer_chat/lib/gemini.dart
@@ -45,10 +45,7 @@ class GeminiState extends State {
);
_chatSession = _model.startChat(
- history: _chatController.messages
- .whereType()
- .map((message) => Content.text(message.text))
- .toList(),
+ history: _chatController.messages.whereType().map((message) => Content.text(message.text)).toList(),
);
}
@@ -65,10 +62,8 @@ class GeminiState extends State {
return Scaffold(
body: Chat(
builders: Builders(
- textMessageBuilder: (context, message) =>
- FlyerChatTextMessage(message: message),
- imageMessageBuilder: (context, message) =>
- FlyerChatImageMessage(message: message),
+ textMessageBuilder: (context, message) => FlyerChatTextMessage(message: message),
+ imageMessageBuilder: (context, message) => FlyerChatImageMessage(message: message),
),
chatController: _chatController,
crossCache: _crossCache,
@@ -156,8 +151,7 @@ class GeminiState extends State {
);
await _chatController.insert(_currentGeminiResponse!);
} else {
- final newUpdatedMessage = (_currentGeminiResponse as TextMessage)
- .copyWith(text: accumulatedText);
+ final newUpdatedMessage = (_currentGeminiResponse as TextMessage).copyWith(text: accumulatedText);
await _chatController.update(
_currentGeminiResponse!,
newUpdatedMessage,
@@ -168,9 +162,7 @@ class GeminiState extends State {
// as soon as message that is being generated reaches top of the viewport
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!_scrollController.hasClients || !mounted) return;
- if ((_scrollController.position.maxScrollExtent -
- initialMaxScrollExtent) <
- viewportDimension) {
+ if ((_scrollController.position.maxScrollExtent - initialMaxScrollExtent) < viewportDimension) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 250),
diff --git a/examples/flyer_chat/lib/hive_chat_controller.dart b/examples/flyer_chat/lib/hive_chat_controller.dart
index dec7c961..a41c336b 100644
--- a/examples/flyer_chat/lib/hive_chat_controller.dart
+++ b/examples/flyer_chat/lib/hive_chat_controller.dart
@@ -26,11 +26,7 @@ class HiveChatController implements ChatController {
@override
Future remove(Message message) async {
- final index = _box
- .getRange(0, _box.length)
- .map((json) => Message.fromJson(json))
- .toList()
- .indexOf(message);
+ final index = _box.getRange(0, _box.length).map((json) => Message.fromJson(json)).toList().indexOf(message);
if (index > -1) {
_box.write(() {
@@ -44,11 +40,7 @@ class HiveChatController implements ChatController {
Future update(Message oldMessage, Message newMessage) async {
if (oldMessage == newMessage) return;
- final index = _box
- .getRange(0, _box.length)
- .map((json) => Message.fromJson(json))
- .toList()
- .indexOf(oldMessage);
+ final index = _box.getRange(0, _box.length).map((json) => Message.fromJson(json)).toList().indexOf(oldMessage);
if (index > -1) {
_box.write(() {
@@ -67,10 +59,7 @@ class HiveChatController implements ChatController {
} else {
_box.write(() {
_box.putAll(
- messages
- .map((message) => {message.id: message.toJson()})
- .toList()
- .reduce((acc, map) => {...acc, ...map}),
+ messages.map((message) => {message.id: message.toJson()}).toList().reduce((acc, map) => {...acc, ...map}),
);
_operationsController.add(ChatOperation.set());
});
@@ -79,10 +68,7 @@ class HiveChatController implements ChatController {
@override
List get messages {
- return _box
- .getRange(0, _box.length)
- .map((json) => Message.fromJson(json))
- .toList();
+ return _box.getRange(0, _box.length).map((json) => Message.fromJson(json)).toList();
}
@override
diff --git a/examples/flyer_chat/lib/local.dart b/examples/flyer_chat/lib/local.dart
index f4ea04bc..065320c8 100644
--- a/examples/flyer_chat/lib/local.dart
+++ b/examples/flyer_chat/lib/local.dart
@@ -1,9 +1,11 @@
+import 'dart:io';
import 'dart:math';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
+import 'package:flyer_chat_audio_message/flyer_chat_audio_message.dart';
import 'package:flyer_chat_image_message/flyer_chat_image_message.dart';
import 'package:flyer_chat_text_message/flyer_chat_text_message.dart';
import 'package:image_picker/image_picker.dart';
@@ -34,18 +36,20 @@ class LocalState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
- body: Chat(
- builders: Builders(
- textMessageBuilder: (context, message) =>
- FlyerChatTextMessage(message: message),
- imageMessageBuilder: (context, message) =>
- FlyerChatImageMessage(message: message),
+ body: SafeArea(
+ child: Chat(
+ builders: Builders(
+ textMessageBuilder: (context, message) => FlyerChatTextMessage(message: message),
+ audioMessageBuilder: (context, message) => FlyerChatAudioMessage(message: message),
+ imageMessageBuilder: (context, message) => FlyerChatImageMessage(message: message),
+ ),
+ chatController: _chatController,
+ user: widget.author,
+ onMessageSend: _addItem,
+ onAudioSend: _addAudio,
+ onMessageTap: _removeItem,
+ onAttachmentTap: _handleAttachmentTap,
),
- chatController: _chatController,
- user: widget.author,
- onMessageSend: _addItem,
- onMessageTap: _removeItem,
- onAttachmentTap: _handleAttachmentTap,
),
persistentFooterButtons: [
TextButton(
@@ -71,9 +75,7 @@ class LocalState extends State {
}
void _addItem(String? text) async {
- final randomUser = Random().nextInt(2) == 0
- ? const User(id: 'sender1')
- : const User(id: 'sender2');
+ final randomUser = Random().nextInt(2) == 0 ? const User(id: 'sender1') : const User(id: 'sender2');
final message = await createMessage(randomUser, widget.dio, text: text);
@@ -102,4 +104,17 @@ class LocalState extends State {
await _chatController.insert(imageMessage);
}
}
+
+ Future _addAudio(File file) async {
+ final message = await createMessage(
+ widget.author,
+ widget.dio,
+ type: MessageType.audio,
+ file: file,
+ );
+
+ if (mounted) {
+ await _chatController.insert(message);
+ }
+ }
}
diff --git a/examples/flyer_chat/lib/main.dart b/examples/flyer_chat/lib/main.dart
index 163ac94f..3eadd3e9 100644
--- a/examples/flyer_chat/lib/main.dart
+++ b/examples/flyer_chat/lib/main.dart
@@ -71,207 +71,209 @@ class _FlyerChatHomePageState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
- body: Center(
- child: SingleChildScrollView(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const SizedBox(height: 8),
- SegmentedButton(
- segments: const >[
- ButtonSegment(
- value: 'sender1',
- label: Text('sender1'),
- ),
- ButtonSegment(
- value: 'sender2',
- label: Text('sender2'),
- ),
- ],
- selected: {_author.id},
- onSelectionChanged: (Set newSender) {
- setState(() {
- // By default there is only a single segment that can be
- // selected at one time, so its value is always the first
- // item in the selected set.
- _author = User(id: newSender.first);
- });
- },
- ),
- const SizedBox(height: 8),
- SizedBox(
- width: 200,
- child: TextField(
- controller: _chatIdController,
- decoration: const InputDecoration(
- border: OutlineInputBorder(),
- hintText: 'chat id',
+ body: SafeArea(
+ child: Center(
+ child: SingleChildScrollView(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const SizedBox(height: 8),
+ SegmentedButton(
+ segments: const >[
+ ButtonSegment(
+ value: 'sender1',
+ label: Text('sender1'),
+ ),
+ ButtonSegment(
+ value: 'sender2',
+ label: Text('sender2'),
+ ),
+ ],
+ selected: {_author.id},
+ onSelectionChanged: (Set newSender) {
+ setState(() {
+ // By default there is only a single segment that can be
+ // selected at one time, so its value is always the first
+ // item in the selected set.
+ _author = User(id: newSender.first);
+ });
+ },
+ ),
+ const SizedBox(height: 8),
+ SizedBox(
+ width: 200,
+ child: TextField(
+ controller: _chatIdController,
+ decoration: const InputDecoration(
+ border: OutlineInputBorder(),
+ hintText: 'chat id',
+ ),
),
),
- ),
- const SizedBox(height: 8),
- Wrap(
- alignment: WrapAlignment.center,
- spacing: 8,
- runSpacing: 8,
- children: [
- ElevatedButton(
- onPressed: () {
- getInitialMessages(_dio, chatId: _chatIdController.text)
- .then((messages) {
- if (mounted && context.mounted) {
- Navigator.of(context).push(
- MaterialPageRoute(
- builder: (context) => Api(
- author: _author,
- chatId: _chatIdController.text,
- initialMessages: messages,
- dio: _dio,
- ),
- ),
- );
- }
- }).catchError((error) {
- if (mounted && context.mounted) {
- debugPrint(error.toString());
- showDialog(
- context: context,
- builder: (BuildContext context) {
- return AlertDialog(
- title: const Text('Error'),
- content: const Text(
- 'Make sure the chat ID is correct',
+ const SizedBox(height: 8),
+ Wrap(
+ alignment: WrapAlignment.center,
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ ElevatedButton(
+ onPressed: () {
+ getInitialMessages(_dio, chatId: _chatIdController.text)
+ .then((messages) {
+ if (mounted && context.mounted) {
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (context) => Api(
+ author: _author,
+ chatId: _chatIdController.text,
+ initialMessages: messages,
+ dio: _dio,
),
- actions: [
- TextButton(
- child: const Text('OK'),
- onPressed: () =>
- Navigator.of(context).pop(),
- ),
- ],
- );
- },
- );
- }
- });
- },
- child: const Text('api'),
- ),
- ElevatedButton(
- onPressed: () {
- showDialog(
- context: context,
- builder: (BuildContext context) {
- return AlertDialog(
- title: const Text('Confirmation'),
- content: const Text(
- 'Are you sure you want to generate a new chat ID?',
- ),
- actions: [
- TextButton(
- child: const Text('Cancel'),
- onPressed: () => Navigator.of(context).pop(),
),
- TextButton(
- child: const Text('Yes'),
- onPressed: () {
- Navigator.of(context).pop();
- getChatId(_dio).then((chatId) {
- if (mounted && context.mounted) {
- _chatIdController.text = chatId;
- }
- });
- },
+ );
+ }
+ }).catchError((error) {
+ if (mounted && context.mounted) {
+ debugPrint(error.toString());
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: const Text('Error'),
+ content: const Text(
+ 'Make sure the chat ID is correct',
+ ),
+ actions: [
+ TextButton(
+ child: const Text('OK'),
+ onPressed: () =>
+ Navigator.of(context).pop(),
+ ),
+ ],
+ );
+ },
+ );
+ }
+ });
+ },
+ child: const Text('api'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: const Text('Confirmation'),
+ content: const Text(
+ 'Are you sure you want to generate a new chat ID?',
),
- ],
- );
- },
- );
- },
- child: const Text('generate chat id'),
- ),
- ElevatedButton(
- onPressed: () {
- Clipboard.setData(
- ClipboardData(text: _chatIdController.text),
- );
- },
- child: const Text('copy chat id'),
+ actions: [
+ TextButton(
+ child: const Text('Cancel'),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ TextButton(
+ child: const Text('Yes'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ getChatId(_dio).then((chatId) {
+ if (mounted && context.mounted) {
+ _chatIdController.text = chatId;
+ }
+ });
+ },
+ ),
+ ],
+ );
+ },
+ );
+ },
+ child: const Text('generate chat id'),
+ ),
+ ElevatedButton(
+ onPressed: () {
+ Clipboard.setData(
+ ClipboardData(text: _chatIdController.text),
+ );
+ },
+ child: const Text('copy chat id'),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16),
+ child: Text(
+ 'In order to test the api, you need to generate a chat id. Chat will be reset after 24 hours. Use the same chat id to access chat on different devices.',
+ textAlign: TextAlign.center,
),
- ],
- ),
- const SizedBox(height: 8),
- const Padding(
- padding: EdgeInsets.symmetric(horizontal: 16),
- child: Text(
- 'In order to test the api, you need to generate a chat id. Chat will be reset after 24 hours. Use the same chat id to access chat on different devices.',
- textAlign: TextAlign.center,
),
- ),
- const SizedBox(
- width: 200,
- child: Padding(
- padding: EdgeInsets.symmetric(vertical: 8),
- child: Divider(
- color: Colors.grey,
- thickness: 1,
+ const SizedBox(
+ width: 200,
+ child: Padding(
+ padding: EdgeInsets.symmetric(vertical: 8),
+ child: Divider(
+ color: Colors.grey,
+ thickness: 1,
+ ),
),
),
- ),
- ElevatedButton(
- onPressed: () {
- Navigator.of(context).push(
- MaterialPageRoute(
- builder: (context) => Local(author: _author, dio: _dio),
+ ElevatedButton(
+ onPressed: () {
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (context) => Local(author: _author, dio: _dio),
+ ),
+ );
+ },
+ child: const Text('local'),
+ ),
+ const SizedBox(
+ width: 200,
+ child: Padding(
+ padding: EdgeInsets.symmetric(vertical: 8),
+ child: Divider(
+ color: Colors.grey,
+ thickness: 1,
),
- );
- },
- child: const Text('local'),
- ),
- const SizedBox(
- width: 200,
- child: Padding(
- padding: EdgeInsets.symmetric(vertical: 8),
- child: Divider(
- color: Colors.grey,
- thickness: 1,
),
),
- ),
- SizedBox(
- width: 200,
- child: TextField(
- controller: _geminiApiKeyController,
- decoration: const InputDecoration(
- border: OutlineInputBorder(),
- hintText: 'gemini api key',
+ SizedBox(
+ width: 200,
+ child: TextField(
+ controller: _geminiApiKeyController,
+ decoration: const InputDecoration(
+ border: OutlineInputBorder(),
+ hintText: 'gemini api key',
+ ),
),
),
- ),
- const SizedBox(height: 8),
- ElevatedButton(
- onPressed: () {
- Navigator.of(context).push(
- MaterialPageRoute(
- builder: (context) => Gemini(
- geminiApiKey: _geminiApiKeyController.text,
- database: widget.database,
+ const SizedBox(height: 8),
+ ElevatedButton(
+ onPressed: () {
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (context) => Gemini(
+ geminiApiKey: _geminiApiKeyController.text,
+ database: widget.database,
+ ),
),
- ),
- );
- },
- child: const Text('gemini'),
- ),
- const SizedBox(height: 8),
- const Padding(
- padding: EdgeInsets.symmetric(horizontal: 16),
- child: Text(
- 'In order to test the AI example, you need to provide your own Gemini API key',
- textAlign: TextAlign.center,
+ );
+ },
+ child: const Text('gemini'),
+ ),
+ const SizedBox(height: 8),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16),
+ child: Text(
+ 'In order to test the AI example, you need to provide your own Gemini API key',
+ textAlign: TextAlign.center,
+ ),
),
- ),
- const SizedBox(height: 8),
- ],
+ const SizedBox(height: 8),
+ ],
+ ),
),
),
),
diff --git a/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc b/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc
index 02bc14a7..b42b2618 100644
--- a/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc
+++ b/examples/flyer_chat/linux/flutter/generated_plugin_registrant.cc
@@ -6,14 +6,22 @@
#include "generated_plugin_registrant.h"
+#include
#include
#include
+#include
void fl_register_plugins(FlPluginRegistry* registry) {
+ g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
+ audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin");
isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar);
+ g_autoptr(FlPluginRegistrar) record_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
+ record_linux_plugin_register_with_registrar(record_linux_registrar);
}
diff --git a/examples/flyer_chat/linux/flutter/generated_plugins.cmake b/examples/flyer_chat/linux/flutter/generated_plugins.cmake
index 00bef184..fb848f3a 100644
--- a/examples/flyer_chat/linux/flutter/generated_plugins.cmake
+++ b/examples/flyer_chat/linux/flutter/generated_plugins.cmake
@@ -3,8 +3,10 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ audioplayers_linux
file_selector_linux
isar_flutter_libs
+ record_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
diff --git a/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift
index 0b202afd..d9caf934 100644
--- a/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/examples/flyer_chat/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,12 +5,16 @@
import FlutterMacOS
import Foundation
+import audioplayers_darwin
import file_selector_macos
import isar_flutter_libs
import path_provider_foundation
+import record_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
+ RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin"))
}
diff --git a/examples/flyer_chat/macos/Runner/Info.plist b/examples/flyer_chat/macos/Runner/Info.plist
index 4789daa6..db5da9ed 100644
--- a/examples/flyer_chat/macos/Runner/Info.plist
+++ b/examples/flyer_chat/macos/Runner/Info.plist
@@ -28,5 +28,9 @@
MainMenu
NSPrincipalClass
NSApplication
+ NSMicrophoneUsageDescription
+ Some message to describe why you need this permission
+ com.apple.security.device.audio-input
+
diff --git a/examples/flyer_chat/pubspec.yaml b/examples/flyer_chat/pubspec.yaml
index 914f16d8..3fe82e31 100644
--- a/examples/flyer_chat/pubspec.yaml
+++ b/examples/flyer_chat/pubspec.yaml
@@ -38,6 +38,7 @@ dependencies:
flutter_chat_core: ^0.0.2
flutter_chat_ui: ^2.0.0-dev.1
flutter_lorem: ^2.0.0
+ flyer_chat_audio_message: ^0.0.2
flyer_chat_image_message: ^0.0.2
flyer_chat_text_message: ^0.0.2
google_generative_ai: ^0.4.5
diff --git a/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc b/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc
index df32f874..439d5f4b 100644
--- a/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc
+++ b/examples/flyer_chat/windows/flutter/generated_plugin_registrant.cc
@@ -6,12 +6,21 @@
#include "generated_plugin_registrant.h"
+#include
#include
#include
+#include
+#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
+ AudioplayersWindowsPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
IsarFlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
+ PermissionHandlerWindowsPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
+ RecordWindowsPluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
}
diff --git a/examples/flyer_chat/windows/flutter/generated_plugins.cmake b/examples/flyer_chat/windows/flutter/generated_plugins.cmake
index 52869ab3..1cc4d573 100644
--- a/examples/flyer_chat/windows/flutter/generated_plugins.cmake
+++ b/examples/flyer_chat/windows/flutter/generated_plugins.cmake
@@ -3,8 +3,11 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ audioplayers_windows
file_selector_windows
isar_flutter_libs
+ permission_handler_windows
+ record_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
diff --git a/packages/flutter_chat_core/lib/src/models/builders.dart b/packages/flutter_chat_core/lib/src/models/builders.dart
index 4dd041ae..d2edecfd 100644
--- a/packages/flutter_chat_core/lib/src/models/builders.dart
+++ b/packages/flutter_chat_core/lib/src/models/builders.dart
@@ -6,6 +6,7 @@ import 'message.dart';
part 'builders.freezed.dart';
typedef TextMessageBuilder = Widget Function(BuildContext, TextMessage);
+typedef AudioMessageBuilder = Widget Function(BuildContext, AudioMessage);
typedef ImageMessageBuilder = Widget Function(BuildContext, ImageMessage);
typedef UnsupportedMessageBuilder = Widget Function(
BuildContext,
@@ -34,6 +35,7 @@ class Builders with _$Builders {
const factory Builders({
TextMessageBuilder? textMessageBuilder,
ImageMessageBuilder? imageMessageBuilder,
+ AudioMessageBuilder? audioMessageBuilder,
UnsupportedMessageBuilder? unsupportedMessageBuilder,
InputBuilder? inputBuilder,
ChatMessageBuilder? chatMessageBuilder,
diff --git a/packages/flutter_chat_core/lib/src/models/builders.freezed.dart b/packages/flutter_chat_core/lib/src/models/builders.freezed.dart
index ff9a9871..67966bcb 100644
--- a/packages/flutter_chat_core/lib/src/models/builders.freezed.dart
+++ b/packages/flutter_chat_core/lib/src/models/builders.freezed.dart
@@ -20,6 +20,8 @@ mixin _$Builders {
throw _privateConstructorUsedError;
ImageMessageBuilder? get imageMessageBuilder =>
throw _privateConstructorUsedError;
+ AudioMessageBuilder? get audioMessageBuilder =>
+ throw _privateConstructorUsedError;
UnsupportedMessageBuilder? get unsupportedMessageBuilder =>
throw _privateConstructorUsedError;
InputBuilder? get inputBuilder => throw _privateConstructorUsedError;
@@ -45,6 +47,7 @@ abstract class $BuildersCopyWith<$Res> {
$Res call(
{TextMessageBuilder? textMessageBuilder,
ImageMessageBuilder? imageMessageBuilder,
+ AudioMessageBuilder? audioMessageBuilder,
UnsupportedMessageBuilder? unsupportedMessageBuilder,
InputBuilder? inputBuilder,
ChatMessageBuilder? chatMessageBuilder,
@@ -69,6 +72,7 @@ class _$BuildersCopyWithImpl<$Res, $Val extends Builders>
$Res call({
Object? textMessageBuilder = freezed,
Object? imageMessageBuilder = freezed,
+ Object? audioMessageBuilder = freezed,
Object? unsupportedMessageBuilder = freezed,
Object? inputBuilder = freezed,
Object? chatMessageBuilder = freezed,
@@ -84,6 +88,10 @@ class _$BuildersCopyWithImpl<$Res, $Val extends Builders>
? _value.imageMessageBuilder
: imageMessageBuilder // ignore: cast_nullable_to_non_nullable
as ImageMessageBuilder?,
+ audioMessageBuilder: freezed == audioMessageBuilder
+ ? _value.audioMessageBuilder
+ : audioMessageBuilder // ignore: cast_nullable_to_non_nullable
+ as AudioMessageBuilder?,
unsupportedMessageBuilder: freezed == unsupportedMessageBuilder
? _value.unsupportedMessageBuilder
: unsupportedMessageBuilder // ignore: cast_nullable_to_non_nullable
@@ -119,6 +127,7 @@ abstract class _$$BuildersImplCopyWith<$Res>
$Res call(
{TextMessageBuilder? textMessageBuilder,
ImageMessageBuilder? imageMessageBuilder,
+ AudioMessageBuilder? audioMessageBuilder,
UnsupportedMessageBuilder? unsupportedMessageBuilder,
InputBuilder? inputBuilder,
ChatMessageBuilder? chatMessageBuilder,
@@ -141,6 +150,7 @@ class __$$BuildersImplCopyWithImpl<$Res>
$Res call({
Object? textMessageBuilder = freezed,
Object? imageMessageBuilder = freezed,
+ Object? audioMessageBuilder = freezed,
Object? unsupportedMessageBuilder = freezed,
Object? inputBuilder = freezed,
Object? chatMessageBuilder = freezed,
@@ -156,6 +166,10 @@ class __$$BuildersImplCopyWithImpl<$Res>
? _value.imageMessageBuilder
: imageMessageBuilder // ignore: cast_nullable_to_non_nullable
as ImageMessageBuilder?,
+ audioMessageBuilder: freezed == audioMessageBuilder
+ ? _value.audioMessageBuilder
+ : audioMessageBuilder // ignore: cast_nullable_to_non_nullable
+ as AudioMessageBuilder?,
unsupportedMessageBuilder: freezed == unsupportedMessageBuilder
? _value.unsupportedMessageBuilder
: unsupportedMessageBuilder // ignore: cast_nullable_to_non_nullable
@@ -186,6 +200,7 @@ class _$BuildersImpl extends _Builders {
const _$BuildersImpl(
{this.textMessageBuilder,
this.imageMessageBuilder,
+ this.audioMessageBuilder,
this.unsupportedMessageBuilder,
this.inputBuilder,
this.chatMessageBuilder,
@@ -198,6 +213,8 @@ class _$BuildersImpl extends _Builders {
@override
final ImageMessageBuilder? imageMessageBuilder;
@override
+ final AudioMessageBuilder? audioMessageBuilder;
+ @override
final UnsupportedMessageBuilder? unsupportedMessageBuilder;
@override
final InputBuilder? inputBuilder;
@@ -210,7 +227,7 @@ class _$BuildersImpl extends _Builders {
@override
String toString() {
- return 'Builders(textMessageBuilder: $textMessageBuilder, imageMessageBuilder: $imageMessageBuilder, unsupportedMessageBuilder: $unsupportedMessageBuilder, inputBuilder: $inputBuilder, chatMessageBuilder: $chatMessageBuilder, chatAnimatedListBuilder: $chatAnimatedListBuilder, scrollToBottomBuilder: $scrollToBottomBuilder)';
+ return 'Builders(textMessageBuilder: $textMessageBuilder, imageMessageBuilder: $imageMessageBuilder, audioMessageBuilder: $audioMessageBuilder, unsupportedMessageBuilder: $unsupportedMessageBuilder, inputBuilder: $inputBuilder, chatMessageBuilder: $chatMessageBuilder, chatAnimatedListBuilder: $chatAnimatedListBuilder, scrollToBottomBuilder: $scrollToBottomBuilder)';
}
@override
@@ -222,6 +239,8 @@ class _$BuildersImpl extends _Builders {
other.textMessageBuilder == textMessageBuilder) &&
(identical(other.imageMessageBuilder, imageMessageBuilder) ||
other.imageMessageBuilder == imageMessageBuilder) &&
+ (identical(other.audioMessageBuilder, audioMessageBuilder) ||
+ other.audioMessageBuilder == audioMessageBuilder) &&
(identical(other.unsupportedMessageBuilder,
unsupportedMessageBuilder) ||
other.unsupportedMessageBuilder == unsupportedMessageBuilder) &&
@@ -241,6 +260,7 @@ class _$BuildersImpl extends _Builders {
runtimeType,
textMessageBuilder,
imageMessageBuilder,
+ audioMessageBuilder,
unsupportedMessageBuilder,
inputBuilder,
chatMessageBuilder,
@@ -260,6 +280,7 @@ abstract class _Builders extends Builders {
const factory _Builders(
{final TextMessageBuilder? textMessageBuilder,
final ImageMessageBuilder? imageMessageBuilder,
+ final AudioMessageBuilder? audioMessageBuilder,
final UnsupportedMessageBuilder? unsupportedMessageBuilder,
final InputBuilder? inputBuilder,
final ChatMessageBuilder? chatMessageBuilder,
@@ -272,6 +293,8 @@ abstract class _Builders extends Builders {
@override
ImageMessageBuilder? get imageMessageBuilder;
@override
+ AudioMessageBuilder? get audioMessageBuilder;
+ @override
UnsupportedMessageBuilder? get unsupportedMessageBuilder;
@override
InputBuilder? get inputBuilder;
diff --git a/packages/flutter_chat_core/lib/src/models/message.dart b/packages/flutter_chat_core/lib/src/models/message.dart
index 40a89650..d4bdbd0a 100644
--- a/packages/flutter_chat_core/lib/src/models/message.dart
+++ b/packages/flutter_chat_core/lib/src/models/message.dart
@@ -30,6 +30,14 @@ sealed class Message with _$Message {
double? height,
}) = ImageMessage;
+ const factory Message.audio({
+ required String id,
+ required User author,
+ Map? metadata,
+ @EpochDateTimeConverter() required DateTime createdAt,
+ required String audioFile,
+ }) = AudioMessage;
+
const factory Message.unsupported({
required String id,
required User author,
@@ -39,6 +47,5 @@ sealed class Message with _$Message {
const Message._();
- factory Message.fromJson(Map json) =>
- _$MessageFromJson(json);
+ factory Message.fromJson(Map json) => _$MessageFromJson(json);
}
diff --git a/packages/flutter_chat_core/lib/src/models/message.freezed.dart b/packages/flutter_chat_core/lib/src/models/message.freezed.dart
index eacfcc68..14072447 100644
--- a/packages/flutter_chat_core/lib/src/models/message.freezed.dart
+++ b/packages/flutter_chat_core/lib/src/models/message.freezed.dart
@@ -20,6 +20,8 @@ Message _$MessageFromJson(Map json) {
return TextMessage.fromJson(json);
case 'image':
return ImageMessage.fromJson(json);
+ case 'audio':
+ return AudioMessage.fromJson(json);
default:
return UnsupportedMessage.fromJson(json);
@@ -54,6 +56,13 @@ mixin _$Message {
double? width,
double? height)
image,
+ required TResult Function(
+ String id,
+ User author,
+ Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt,
+ String audioFile)
+ audio,
required TResult Function(
String id,
User author,
@@ -83,6 +92,9 @@ mixin _$Message {
double? width,
double? height)?
image,
+ TResult? Function(String id, User author, Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt, String audioFile)?
+ audio,
TResult? Function(
String id,
User author,
@@ -112,6 +124,9 @@ mixin _$Message {
double? width,
double? height)?
image,
+ TResult Function(String id, User author, Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt, String audioFile)?
+ audio,
TResult Function(
String id,
User author,
@@ -125,6 +140,7 @@ mixin _$Message {
TResult map({
required TResult Function(TextMessage value) text,
required TResult Function(ImageMessage value) image,
+ required TResult Function(AudioMessage value) audio,
required TResult Function(UnsupportedMessage value) unsupported,
}) =>
throw _privateConstructorUsedError;
@@ -132,6 +148,7 @@ mixin _$Message {
TResult? mapOrNull({
TResult? Function(TextMessage value)? text,
TResult? Function(ImageMessage value)? image,
+ TResult? Function(AudioMessage value)? audio,
TResult? Function(UnsupportedMessage value)? unsupported,
}) =>
throw _privateConstructorUsedError;
@@ -139,6 +156,7 @@ mixin _$Message {
TResult maybeMap({
TResult Function(TextMessage value)? text,
TResult Function(ImageMessage value)? image,
+ TResult Function(AudioMessage value)? audio,
TResult Function(UnsupportedMessage value)? unsupported,
required TResult orElse(),
}) =>
@@ -400,6 +418,13 @@ class _$TextMessageImpl extends TextMessage {
double? width,
double? height)
image,
+ required TResult Function(
+ String id,
+ User author,
+ Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt,
+ String audioFile)
+ audio,
required TResult Function(
String id,
User author,
@@ -432,6 +457,9 @@ class _$TextMessageImpl extends TextMessage {
double? width,
double? height)?
image,
+ TResult? Function(String id, User author, Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt, String audioFile)?
+ audio,
TResult? Function(
String id,
User author,
@@ -464,6 +492,9 @@ class _$TextMessageImpl extends TextMessage {
double? width,
double? height)?
image,
+ TResult Function(String id, User author, Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt, String audioFile)?
+ audio,
TResult Function(
String id,
User author,
@@ -483,6 +514,7 @@ class _$TextMessageImpl extends TextMessage {
TResult map({
required TResult Function(TextMessage value) text,
required TResult Function(ImageMessage value) image,
+ required TResult Function(AudioMessage value) audio,
required TResult Function(UnsupportedMessage value) unsupported,
}) {
return text(this);
@@ -493,6 +525,7 @@ class _$TextMessageImpl extends TextMessage {
TResult? mapOrNull({
TResult? Function(TextMessage value)? text,
TResult? Function(ImageMessage value)? image,
+ TResult? Function(AudioMessage value)? audio,
TResult? Function(UnsupportedMessage value)? unsupported,
}) {
return text?.call(this);
@@ -503,6 +536,7 @@ class _$TextMessageImpl extends TextMessage {
TResult maybeMap({
TResult Function(TextMessage value)? text,
TResult Function(ImageMessage value)? image,
+ TResult Function(AudioMessage value)? audio,
TResult Function(UnsupportedMessage value)? unsupported,
required TResult orElse(),
}) {
@@ -760,6 +794,13 @@ class _$ImageMessageImpl extends ImageMessage {
double? width,
double? height)
image,
+ required TResult Function(
+ String id,
+ User author,
+ Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt,
+ String audioFile)
+ audio,
required TResult Function(
String id,
User author,
@@ -793,6 +834,9 @@ class _$ImageMessageImpl extends ImageMessage {
double? width,
double? height)?
image,
+ TResult? Function(String id, User author, Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt, String audioFile)?
+ audio,
TResult? Function(
String id,
User author,
@@ -826,6 +870,9 @@ class _$ImageMessageImpl extends ImageMessage {
double? width,
double? height)?
image,
+ TResult Function(String id, User author, Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt, String audioFile)?
+ audio,
TResult Function(
String id,
User author,
@@ -846,6 +893,7 @@ class _$ImageMessageImpl extends ImageMessage {
TResult map({
required TResult Function(TextMessage value) text,
required TResult Function(ImageMessage value) image,
+ required TResult Function(AudioMessage value) audio,
required TResult Function(UnsupportedMessage value) unsupported,
}) {
return image(this);
@@ -856,6 +904,7 @@ class _$ImageMessageImpl extends ImageMessage {
TResult? mapOrNull({
TResult? Function(TextMessage value)? text,
TResult? Function(ImageMessage value)? image,
+ TResult? Function(AudioMessage value)? audio,
TResult? Function(UnsupportedMessage value)? unsupported,
}) {
return image?.call(this);
@@ -866,6 +915,7 @@ class _$ImageMessageImpl extends ImageMessage {
TResult maybeMap({
TResult Function(TextMessage value)? text,
TResult Function(ImageMessage value)? image,
+ TResult Function(AudioMessage value)? audio,
TResult Function(UnsupportedMessage value)? unsupported,
required TResult orElse(),
}) {
@@ -922,6 +972,330 @@ abstract class ImageMessage extends Message {
throw _privateConstructorUsedError;
}
+/// @nodoc
+abstract class _$$AudioMessageImplCopyWith<$Res>
+ implements $MessageCopyWith<$Res> {
+ factory _$$AudioMessageImplCopyWith(
+ _$AudioMessageImpl value, $Res Function(_$AudioMessageImpl) then) =
+ __$$AudioMessageImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call(
+ {String id,
+ User author,
+ Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt,
+ String audioFile});
+
+ @override
+ $UserCopyWith<$Res> get author;
+}
+
+/// @nodoc
+class __$$AudioMessageImplCopyWithImpl<$Res>
+ extends _$MessageCopyWithImpl<$Res, _$AudioMessageImpl>
+ implements _$$AudioMessageImplCopyWith<$Res> {
+ __$$AudioMessageImplCopyWithImpl(
+ _$AudioMessageImpl _value, $Res Function(_$AudioMessageImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of Message
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? id = null,
+ Object? author = null,
+ Object? metadata = freezed,
+ Object? createdAt = null,
+ Object? audioFile = null,
+ }) {
+ return _then(_$AudioMessageImpl(
+ id: null == id
+ ? _value.id
+ : id // ignore: cast_nullable_to_non_nullable
+ as String,
+ author: null == author
+ ? _value.author
+ : author // ignore: cast_nullable_to_non_nullable
+ as User,
+ metadata: freezed == metadata
+ ? _value._metadata
+ : metadata // ignore: cast_nullable_to_non_nullable
+ as Map?,
+ createdAt: null == createdAt
+ ? _value.createdAt
+ : createdAt // ignore: cast_nullable_to_non_nullable
+ as DateTime,
+ audioFile: null == audioFile
+ ? _value.audioFile
+ : audioFile // ignore: cast_nullable_to_non_nullable
+ as String,
+ ));
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$AudioMessageImpl extends AudioMessage {
+ const _$AudioMessageImpl(
+ {required this.id,
+ required this.author,
+ final Map? metadata,
+ @EpochDateTimeConverter() required this.createdAt,
+ required this.audioFile,
+ final String? $type})
+ : _metadata = metadata,
+ $type = $type ?? 'audio',
+ super._();
+
+ factory _$AudioMessageImpl.fromJson(Map json) =>
+ _$$AudioMessageImplFromJson(json);
+
+ @override
+ final String id;
+ @override
+ final User author;
+ final Map? _metadata;
+ @override
+ Map? get metadata {
+ final value = _metadata;
+ if (value == null) return null;
+ if (_metadata is EqualUnmodifiableMapView) return _metadata;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableMapView(value);
+ }
+
+ @override
+ @EpochDateTimeConverter()
+ final DateTime createdAt;
+ @override
+ final String audioFile;
+
+ @JsonKey(name: 'type')
+ final String $type;
+
+ @override
+ String toString() {
+ return 'Message.audio(id: $id, author: $author, metadata: $metadata, createdAt: $createdAt, audioFile: $audioFile)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$AudioMessageImpl &&
+ (identical(other.id, id) || other.id == id) &&
+ (identical(other.author, author) || other.author == author) &&
+ const DeepCollectionEquality().equals(other._metadata, _metadata) &&
+ (identical(other.createdAt, createdAt) ||
+ other.createdAt == createdAt) &&
+ (identical(other.audioFile, audioFile) ||
+ other.audioFile == audioFile));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, id, author,
+ const DeepCollectionEquality().hash(_metadata), createdAt, audioFile);
+
+ /// Create a copy of Message
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$AudioMessageImplCopyWith<_$AudioMessageImpl> get copyWith =>
+ __$$AudioMessageImplCopyWithImpl<_$AudioMessageImpl>(this, _$identity);
+
+ @override
+ @optionalTypeArgs
+ TResult when({
+ required TResult Function(
+ String id,
+ User author,
+ @EpochDateTimeConverter() DateTime createdAt,
+ Map? metadata,
+ String text,
+ LinkPreview? linkPreview)
+ text,
+ required TResult Function(
+ String id,
+ User author,
+ @EpochDateTimeConverter() DateTime createdAt,
+ Map? metadata,
+ String source,
+ String? thumbhash,
+ String? blurhash,
+ double? width,
+ double? height)
+ image,
+ required TResult Function(
+ String id,
+ User author,
+ Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt,
+ String audioFile)
+ audio,
+ required TResult Function(
+ String id,
+ User author,
+ @EpochDateTimeConverter() DateTime createdAt,
+ Map? metadata)
+ unsupported,
+ }) {
+ return audio(id, author, metadata, createdAt, audioFile);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult? whenOrNull({
+ TResult? Function(
+ String id,
+ User author,
+ @EpochDateTimeConverter() DateTime createdAt,
+ Map? metadata,
+ String text,
+ LinkPreview? linkPreview)?
+ text,
+ TResult? Function(
+ String id,
+ User author,
+ @EpochDateTimeConverter() DateTime createdAt,
+ Map? metadata,
+ String source,
+ String? thumbhash,
+ String? blurhash,
+ double? width,
+ double? height)?
+ image,
+ TResult? Function(String id, User author, Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt, String audioFile)?
+ audio,
+ TResult? Function(
+ String id,
+ User author,
+ @EpochDateTimeConverter() DateTime createdAt,
+ Map? metadata)?
+ unsupported,
+ }) {
+ return audio?.call(id, author, metadata, createdAt, audioFile);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult maybeWhen({
+ TResult Function(
+ String id,
+ User author,
+ @EpochDateTimeConverter() DateTime createdAt,
+ Map? metadata,
+ String text,
+ LinkPreview? linkPreview)?
+ text,
+ TResult Function(
+ String id,
+ User author,
+ @EpochDateTimeConverter() DateTime createdAt,
+ Map? metadata,
+ String source,
+ String? thumbhash,
+ String? blurhash,
+ double? width,
+ double? height)?
+ image,
+ TResult Function(String id, User author, Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt, String audioFile)?
+ audio,
+ TResult Function(
+ String id,
+ User author,
+ @EpochDateTimeConverter() DateTime createdAt,
+ Map? metadata)?
+ unsupported,
+ required TResult orElse(),
+ }) {
+ if (audio != null) {
+ return audio(id, author, metadata, createdAt, audioFile);
+ }
+ return orElse();
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult map({
+ required TResult Function(TextMessage value) text,
+ required TResult Function(ImageMessage value) image,
+ required TResult Function(AudioMessage value) audio,
+ required TResult Function(UnsupportedMessage value) unsupported,
+ }) {
+ return audio(this);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult? mapOrNull({
+ TResult? Function(TextMessage value)? text,
+ TResult? Function(ImageMessage value)? image,
+ TResult? Function(AudioMessage value)? audio,
+ TResult? Function(UnsupportedMessage value)? unsupported,
+ }) {
+ return audio?.call(this);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult maybeMap({
+ TResult Function(TextMessage value)? text,
+ TResult Function(ImageMessage value)? image,
+ TResult Function(AudioMessage value)? audio,
+ TResult Function(UnsupportedMessage value)? unsupported,
+ required TResult orElse(),
+ }) {
+ if (audio != null) {
+ return audio(this);
+ }
+ return orElse();
+ }
+
+ @override
+ Map toJson() {
+ return _$$AudioMessageImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class AudioMessage extends Message {
+ const factory AudioMessage(
+ {required final String id,
+ required final User author,
+ final Map? metadata,
+ @EpochDateTimeConverter() required final DateTime createdAt,
+ required final String audioFile}) = _$AudioMessageImpl;
+ const AudioMessage._() : super._();
+
+ factory AudioMessage.fromJson(Map json) =
+ _$AudioMessageImpl.fromJson;
+
+ @override
+ String get id;
+ @override
+ User get author;
+ @override
+ Map? get metadata;
+ @override
+ @EpochDateTimeConverter()
+ DateTime get createdAt;
+ String get audioFile;
+
+ /// Create a copy of Message
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$AudioMessageImplCopyWith<_$AudioMessageImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
/// @nodoc
abstract class _$$UnsupportedMessageImplCopyWith<$Res>
implements $MessageCopyWith<$Res> {
@@ -1068,6 +1442,13 @@ class _$UnsupportedMessageImpl extends UnsupportedMessage {
double? width,
double? height)
image,
+ required TResult Function(
+ String id,
+ User author,
+ Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt,
+ String audioFile)
+ audio,
required TResult Function(
String id,
User author,
@@ -1100,6 +1481,9 @@ class _$UnsupportedMessageImpl extends UnsupportedMessage {
double? width,
double? height)?
image,
+ TResult? Function(String id, User author, Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt, String audioFile)?
+ audio,
TResult? Function(
String id,
User author,
@@ -1132,6 +1516,9 @@ class _$UnsupportedMessageImpl extends UnsupportedMessage {
double? width,
double? height)?
image,
+ TResult Function(String id, User author, Map? metadata,
+ @EpochDateTimeConverter() DateTime createdAt, String audioFile)?
+ audio,
TResult Function(
String id,
User author,
@@ -1151,6 +1538,7 @@ class _$UnsupportedMessageImpl extends UnsupportedMessage {
TResult map({
required TResult Function(TextMessage value) text,
required TResult Function(ImageMessage value) image,
+ required TResult Function(AudioMessage value) audio,
required TResult Function(UnsupportedMessage value) unsupported,
}) {
return unsupported(this);
@@ -1161,6 +1549,7 @@ class _$UnsupportedMessageImpl extends UnsupportedMessage {
TResult? mapOrNull({
TResult? Function(TextMessage value)? text,
TResult? Function(ImageMessage value)? image,
+ TResult? Function(AudioMessage value)? audio,
TResult? Function(UnsupportedMessage value)? unsupported,
}) {
return unsupported?.call(this);
@@ -1171,6 +1560,7 @@ class _$UnsupportedMessageImpl extends UnsupportedMessage {
TResult maybeMap({
TResult Function(TextMessage value)? text,
TResult Function(ImageMessage value)? image,
+ TResult Function(AudioMessage value)? audio,
TResult Function(UnsupportedMessage value)? unsupported,
required TResult orElse(),
}) {
diff --git a/packages/flutter_chat_core/lib/src/models/message.g.dart b/packages/flutter_chat_core/lib/src/models/message.g.dart
index 359927c5..987bcfbd 100644
--- a/packages/flutter_chat_core/lib/src/models/message.g.dart
+++ b/packages/flutter_chat_core/lib/src/models/message.g.dart
@@ -78,6 +78,36 @@ Map _$$ImageMessageImplToJson(_$ImageMessageImpl instance) {
return val;
}
+_$AudioMessageImpl _$$AudioMessageImplFromJson(Map json) =>
+ _$AudioMessageImpl(
+ id: json['id'] as String,
+ author: User.fromJson(json['author'] as Map),
+ metadata: json['metadata'] as Map?,
+ createdAt: const EpochDateTimeConverter()
+ .fromJson((json['createdAt'] as num).toInt()),
+ audioFile: json['audioFile'] as String,
+ $type: json['type'] as String?,
+ );
+
+Map _$$AudioMessageImplToJson(_$AudioMessageImpl instance) {
+ final val = {
+ 'id': instance.id,
+ 'author': instance.author.toJson(),
+ };
+
+ void writeNotNull(String key, dynamic value) {
+ if (value != null) {
+ val[key] = value;
+ }
+ }
+
+ writeNotNull('metadata', instance.metadata);
+ val['createdAt'] = const EpochDateTimeConverter().toJson(instance.createdAt);
+ val['audioFile'] = instance.audioFile;
+ val['type'] = instance.$type;
+ return val;
+}
+
_$UnsupportedMessageImpl _$$UnsupportedMessageImplFromJson(
Map json) =>
_$UnsupportedMessageImpl(
diff --git a/packages/flutter_chat_ui/lib/src/chat.dart b/packages/flutter_chat_ui/lib/src/chat.dart
index c3291ed5..30a6f2b9 100644
--- a/packages/flutter_chat_ui/lib/src/chat.dart
+++ b/packages/flutter_chat_ui/lib/src/chat.dart
@@ -20,6 +20,7 @@ class Chat extends StatefulWidget {
final ChatTheme? darkTheme;
final ThemeMode themeMode;
final OnMessageSendCallback? onMessageSend;
+ final OnAudioSendCallback? onAudioSend;
final OnMessageTapCallback? onMessageTap;
final OnAttachmentTapCallback? onAttachmentTap;
@@ -34,6 +35,7 @@ class Chat extends StatefulWidget {
this.darkTheme,
this.themeMode = ThemeMode.system,
this.onMessageSend,
+ this.onAudioSend,
this.onMessageTap,
this.onAttachmentTap,
});
@@ -69,8 +71,7 @@ class _ChatState extends State with WidgetsBindingObserver {
void didUpdateWidget(covariant Chat oldWidget) {
super.didUpdateWidget(oldWidget);
- if (oldWidget.theme != widget.theme ||
- oldWidget.darkTheme != widget.darkTheme) {
+ if (oldWidget.theme != widget.theme || oldWidget.darkTheme != widget.darkTheme) {
_updateTheme(theme: _theme, darkTheme: _theme);
}
@@ -104,6 +105,7 @@ class _ChatState extends State with WidgetsBindingObserver {
Provider.value(value: _builders),
Provider.value(value: _crossCache),
Provider.value(value: widget.onMessageSend),
+ Provider.value(value: widget.onAudioSend),
Provider.value(value: widget.onMessageTap),
Provider.value(value: widget.onAttachmentTap),
ChangeNotifierProvider(create: (_) => ChatInputHeightNotifier()),
@@ -151,10 +153,9 @@ class _ChatState extends State with WidgetsBindingObserver {
_theme = (darkTheme ?? ChatTheme.dark()).merge(widget.darkTheme);
break;
case ThemeMode.system:
- _theme =
- PlatformDispatcher.instance.platformBrightness == Brightness.dark
- ? (darkTheme ?? ChatTheme.dark()).merge(widget.darkTheme)
- : (theme ?? ChatTheme.light()).merge(widget.theme);
+ _theme = PlatformDispatcher.instance.platformBrightness == Brightness.dark
+ ? (darkTheme ?? ChatTheme.dark()).merge(widget.darkTheme)
+ : (theme ?? ChatTheme.light()).merge(widget.theme);
break;
}
}
diff --git a/packages/flutter_chat_ui/lib/src/chat_input.dart b/packages/flutter_chat_ui/lib/src/chat_input.dart
index 24d33c29..68c8e4b4 100644
--- a/packages/flutter_chat_ui/lib/src/chat_input.dart
+++ b/packages/flutter_chat_ui/lib/src/chat_input.dart
@@ -1,26 +1,58 @@
+import 'dart:developer' as developer;
+import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:provider/provider.dart';
+import 'package:record/record.dart';
+import './wave_animation.dart';
import 'utils/chat_input_height_notifier.dart';
import 'utils/typedefs.dart';
+/// A widget that provides a chat input interface with text, attachment, and audio recording capabilities.
class ChatInput extends StatefulWidget {
+ /// The left position of the chat input.
final double? left;
+
+ /// The right position of the chat input.
final double? right;
+
+ /// The top position of the chat input.
final double? top;
+
+ /// The bottom position of the chat input.
final double? bottom;
+
+ /// The horizontal blur radius for the backdrop filter.
final double? sigmaX;
+
+ /// The vertical blur radius for the backdrop filter.
final double? sigmaY;
+
+ /// The padding around the chat input.
final EdgeInsetsGeometry? padding;
+
+ /// The icon for attachments.
final Widget? attachmentIcon;
+
+ /// The icon for sending messages.
final Widget? sendIcon;
+
+ /// The icon for audio recording.
+ final Widget? audioIcon;
+
+ /// The gap between elements in the chat input.
final double? gap;
+
+ /// The border for the text input field.
final InputBorder? inputBorder;
+
+ /// Whether the text input field is filled.
final bool? filled;
+ /// Creates a [ChatInput] widget.
const ChatInput({
super.key,
this.left = 0,
@@ -32,6 +64,7 @@ class ChatInput extends StatefulWidget {
this.padding = const EdgeInsets.all(8.0),
this.attachmentIcon = const Icon(Icons.attachment),
this.sendIcon = const Icon(Icons.send),
+ this.audioIcon = const Icon(Icons.mic),
this.gap = 8,
this.inputBorder = const OutlineInputBorder(
borderSide: BorderSide.none,
@@ -47,6 +80,9 @@ class ChatInput extends StatefulWidget {
class _ChatInputState extends State {
final GlobalKey _inputKey = GlobalKey();
final TextEditingController _textController = TextEditingController();
+ final AudioRecorder _audioRecorder = AudioRecorder();
+ bool _isRecording = false;
+ String? _recordedAudioPath;
@override
void initState() {
@@ -57,13 +93,13 @@ class _ChatInputState extends State {
@override
void dispose() {
_textController.dispose();
+ _audioRecorder.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
- final backgroundColor =
- context.select((ChatTheme theme) => theme.backgroundColor);
+ final backgroundColor = context.select((ChatTheme theme) => theme.backgroundColor);
final inputTheme = context.select((ChatTheme theme) => theme.inputTheme);
final onAttachmentTap = context.read();
@@ -76,7 +112,6 @@ class _ChatInputState extends State {
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(
- // TODO: remove backdrop filter if both are 0
sigmaX: widget.sigmaX ?? 0,
sigmaY: widget.sigmaY ?? 0,
),
@@ -84,43 +119,61 @@ class _ChatInputState extends State {
key: _inputKey,
color: backgroundColor.withOpacity(0.8),
child: Padding(
- // TODO: remove padding if it's 0
padding: widget.padding ?? EdgeInsets.zero,
child: Row(
children: [
- widget.attachmentIcon != null
- ? IconButton(
- icon: widget.attachmentIcon!,
- color: inputTheme.hintStyle?.color,
- onPressed: onAttachmentTap,
- )
- : const SizedBox.shrink(),
- SizedBox(width: widget.gap),
- Expanded(
- child: TextField(
- controller: _textController,
- decoration: InputDecoration(
- hintText: 'Type a message',
- hintStyle: inputTheme.hintStyle,
- border: widget.inputBorder,
- filled: widget.filled,
- fillColor: inputTheme.backgroundColor,
- hoverColor: Colors.transparent,
- ),
- style: inputTheme.textStyle,
- onSubmitted: _handleSubmitted,
- textInputAction: TextInputAction.send,
+ if (widget.attachmentIcon != null && !_isRecording)
+ IconButton(
+ icon: widget.attachmentIcon!,
+ color: inputTheme.hintStyle?.color,
+ onPressed: onAttachmentTap,
),
+ if (!_isRecording) SizedBox(width: widget.gap),
+ Expanded(
+ child: _isRecording
+ ? _buildRecordingIndicator()
+ : TextField(
+ controller: _textController,
+ decoration: InputDecoration(
+ hintText: 'Type a message',
+ hintStyle: inputTheme.hintStyle,
+ border: widget.inputBorder,
+ filled: widget.filled,
+ fillColor: inputTheme.backgroundColor,
+ hoverColor: Colors.transparent,
+ ),
+ style: inputTheme.textStyle,
+ onSubmitted: _handleSubmitted,
+ textInputAction: TextInputAction.send,
+ ),
),
SizedBox(width: widget.gap),
- widget.sendIcon != null
- ? IconButton(
- icon: widget.sendIcon!,
- color: inputTheme.hintStyle?.color,
- onPressed: () =>
- _handleSubmitted(_textController.text),
- )
- : const SizedBox.shrink(),
+ ValueListenableBuilder(
+ valueListenable: _textController,
+ builder: (context, value, child) {
+ return widget.sendIcon != null
+ ? Container(
+ decoration: const BoxDecoration(
+ color: Colors.green,
+ shape: BoxShape.circle,
+ ),
+ child: IconButton(
+ icon: value.text.isNotEmpty || _isRecording ? widget.sendIcon! : widget.audioIcon!,
+ color: inputTheme.hintStyle?.color,
+ onPressed: () async {
+ if (_isRecording) {
+ _handleAudioSubmitted();
+ } else if (value.text.isEmpty) {
+ await _handleRecordAudio();
+ } else {
+ _handleSubmitted(value.text);
+ }
+ },
+ ),
+ )
+ : const SizedBox.shrink();
+ },
+ ),
],
),
),
@@ -130,13 +183,63 @@ class _ChatInputState extends State {
);
}
+ Widget _buildRecordingIndicator() {
+ final streamAmplitude = _audioRecorder.onAmplitudeChanged(const Duration(seconds: 1));
+
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.circle, color: Colors.red),
+ const SizedBox(width: 8),
+ StreamBuilder(
+ stream: Stream.periodic(
+ const Duration(seconds: 1),
+ (count) => Duration(seconds: count),
+ ),
+ builder: (context, snapshot) {
+ final duration = snapshot.data ?? Duration.zero;
+ final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
+ final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
+ return Row(
+ children: [
+ Text(
+ '$minutes:$seconds',
+ style: const TextStyle(
+ color: Colors.red,
+ fontSize: 16,
+ fontWeight: FontWeight.w900,
+ ),
+ ),
+ const SizedBox(width: 8),
+ StreamBuilder(
+ stream: streamAmplitude,
+ builder: (context, snapshot) {
+ final current = snapshot.data?.current ?? 0.0;
+ final max = snapshot.data?.max ?? 0.0;
+ final audioLevel = (current / max).clamp(0.0, 1.0);
+
+ developer.log('current: $current', name: 'ChatInput');
+ developer.log('max: $max', name: 'ChatInput');
+ developer.log('audioLevel: $audioLevel', name: 'ChatInput');
+
+ return BarAnimation(
+ color: Colors.red,
+ audioLevel: audioLevel,
+ );
+ },
+ ),
+ ],
+ );
+ },
+ ),
+ ],
+ );
+ }
+
void _updateInputHeight() {
- final renderBox =
- _inputKey.currentContext?.findRenderObject() as RenderBox?;
+ final renderBox = _inputKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox != null) {
- context
- .read()
- .updateHeight(renderBox.size.height);
+ context.read().updateHeight(renderBox.size.height);
}
}
@@ -146,4 +249,44 @@ class _ChatInputState extends State {
_textController.clear();
}
}
+
+ void _handleAudioSubmitted() {
+ _audioRecorder.stop();
+ setState(() {
+ _isRecording = false;
+ });
+
+ if (_recordedAudioPath!.isNotEmpty) {
+ final audioFile = File(_recordedAudioPath!);
+ context.read()?.call(audioFile);
+ _resetAudioState();
+ }
+ }
+
+ Future _handleRecordAudio() async {
+ try {
+ final hasPermission = await _audioRecorder.hasPermission();
+
+ if (hasPermission) {
+ setState(() {
+ _isRecording = true;
+ });
+
+ _recordedAudioPath = 'audio.mp3';
+
+ await _audioRecorder.start(
+ const RecordConfig(),
+ path: 'assets/${_recordedAudioPath!}',
+ );
+ }
+ } catch (e) {
+ debugPrint('Error during audio recording: $e');
+ }
+ }
+
+ void _resetAudioState() {
+ setState(() {
+ _recordedAudioPath = null;
+ });
+ }
}
diff --git a/packages/flutter_chat_ui/lib/src/chat_message/chat_message_internal.dart b/packages/flutter_chat_ui/lib/src/chat_message/chat_message_internal.dart
index 360b4456..073e7b4d 100644
--- a/packages/flutter_chat_ui/lib/src/chat_message/chat_message_internal.dart
+++ b/packages/flutter_chat_ui/lib/src/chat_message/chat_message_internal.dart
@@ -7,11 +7,18 @@ import 'package:provider/provider.dart';
import '../simple_text_message.dart';
import 'chat_message.dart';
+/// A widget that represents an internal chat message with animation and update capabilities.
class ChatMessageInternal extends StatefulWidget {
+ /// Animation for the message.
final Animation animation;
+
+ /// The message to be displayed.
final Message message;
+
+ /// Indicates if the message is removed.
final bool? isRemoved;
+ /// Creates a [ChatMessageInternal] widget.
const ChatMessageInternal({
super.key,
required this.animation,
@@ -23,6 +30,7 @@ class ChatMessageInternal extends StatefulWidget {
State createState() => ChatMessageInternalState();
}
+/// State for [ChatMessageInternal] that handles message updates and rendering.
class ChatMessageInternalState extends State {
late StreamSubscription? _operationsSubscription;
late Message _updatedMessage;
@@ -36,8 +44,7 @@ class ChatMessageInternalState extends State {
if (widget.isRemoved == true) {
_operationsSubscription = null;
} else {
- final chatController =
- Provider.of(context, listen: false);
+ final chatController = Provider.of(context, listen: false);
_operationsSubscription = chatController.operationsStream.listen((event) {
switch (event.type) {
case ChatOperationType.update:
@@ -63,8 +70,8 @@ class ChatMessageInternalState extends State {
@override
void dispose() {
- super.dispose();
_operationsSubscription?.cancel();
+ super.dispose();
}
@override
@@ -92,11 +99,9 @@ class ChatMessageInternalState extends State {
) {
switch (message) {
case TextMessage():
- return builders.textMessageBuilder?.call(context, message) ??
- SimpleTextMessage(message: message);
+ return builders.textMessageBuilder?.call(context, message) ?? SimpleTextMessage(message: message);
case ImageMessage():
- final result = builders.imageMessageBuilder?.call(context, message) ??
- const SizedBox.shrink();
+ final result = builders.imageMessageBuilder?.call(context, message) ?? const SizedBox.shrink();
assert(
!(result is SizedBox && result.width == 0 && result.height == 0),
'You are trying to display an image message but you have not provided an imageMessageBuilder. '
@@ -104,6 +109,11 @@ class ChatMessageInternalState extends State {
'If you want to use default image message widget, install flyer_chat_image_message package and use FlyerChatImageMessage widget.',
);
return result;
+ case AudioMessage():
+ return builders.audioMessageBuilder?.call(context, message) ??
+ const Text(
+ 'Audio message received.',
+ );
case UnsupportedMessage():
return builders.unsupportedMessageBuilder?.call(context, message) ??
const Text(
diff --git a/packages/flutter_chat_ui/lib/src/utils/typedefs.dart b/packages/flutter_chat_ui/lib/src/utils/typedefs.dart
index 6978adf0..c33ee7e4 100644
--- a/packages/flutter_chat_ui/lib/src/utils/typedefs.dart
+++ b/packages/flutter_chat_ui/lib/src/utils/typedefs.dart
@@ -1,7 +1,9 @@
+import 'dart:io';
import 'dart:ui';
import 'package:flutter_chat_core/flutter_chat_core.dart';
typedef OnMessageTapCallback = void Function(Message message);
typedef OnMessageSendCallback = void Function(String text);
+typedef OnAudioSendCallback = void Function(File file);
typedef OnAttachmentTapCallback = VoidCallback;
diff --git a/packages/flutter_chat_ui/lib/src/wave_animation.dart b/packages/flutter_chat_ui/lib/src/wave_animation.dart
new file mode 100644
index 00000000..4d9d1a8b
--- /dev/null
+++ b/packages/flutter_chat_ui/lib/src/wave_animation.dart
@@ -0,0 +1,38 @@
+import 'package:flutter/material.dart';
+
+/// A widget that displays bars based on audio input level.
+///
+/// This widget is used to indicate ongoing processes like recording by
+/// adjusting the height of bars according to the audio input level.
+class BarAnimation extends StatelessWidget {
+ /// The color of the bars in the animation.
+ final Color color;
+
+ /// The audio input level that affects the bar height.
+ final double audioLevel;
+
+ /// Creates a [BarAnimation] widget.
+ const BarAnimation(
+ {super.key, required this.color, required this.audioLevel,});
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: List.generate(32, (index) {
+ final barHeight = audioLevel * 16.0;
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 1.0),
+ child: Container(
+ width: 4.0,
+ height: barHeight,
+ decoration: BoxDecoration(
+ color: color,
+ borderRadius: BorderRadius.circular(2.0),
+ ),
+ ),
+ );
+ }),
+ );
+ }
+}
diff --git a/packages/flutter_chat_ui/pubspec.yaml b/packages/flutter_chat_ui/pubspec.yaml
index 0ca1541c..e64c6007 100644
--- a/packages/flutter_chat_ui/pubspec.yaml
+++ b/packages/flutter_chat_ui/pubspec.yaml
@@ -16,7 +16,9 @@ dependencies:
flutter:
sdk: flutter
flutter_chat_core: ^0.0.2
+ permission_handler: ^11.3.1
provider: ^6.1.2
+ record: ^5.2.0
dev_dependencies:
flutter_lints: ^4.0.0
diff --git a/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart b/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart
index e69de29b..4925262c 100644
--- a/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart
+++ b/packages/flyer_chat_audio_message/lib/src/flyer_chat_audio_message.dart
@@ -0,0 +1,153 @@
+import 'dart:async';
+import 'package:audioplayers/audioplayers.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_chat_core/flutter_chat_core.dart';
+
+/// A widget that displays an audio message with play/pause functionality.
+class FlyerChatAudioMessage extends StatefulWidget {
+ /// The audio message to be played.
+ final AudioMessage message;
+
+ /// The border radius of the audio message container.
+ final BorderRadiusGeometry? borderRadius;
+
+ /// The constraints for the audio message container.
+ final BoxConstraints? constraints;
+
+ /// Creates a [FlyerChatAudioMessage] widget.
+ const FlyerChatAudioMessage({
+ super.key,
+ required this.message,
+ this.borderRadius = const BorderRadius.all(Radius.circular(12)),
+ this.constraints = const BoxConstraints(maxWidth: 300, minHeight: 50),
+ });
+
+ @override
+ FlyerChatAudioMessageState createState() => FlyerChatAudioMessageState();
+}
+
+class FlyerChatAudioMessageState extends State {
+ late final AudioPlayer _player;
+ PlayerState? _playerState;
+ Duration? _currentPosition;
+ Duration? _totalDuration;
+
+ StreamSubscription? _durationSubscription;
+ StreamSubscription? _positionSubscription;
+ StreamSubscription? _playerCompleteSubscription;
+ StreamSubscription? _playerStateChangeSubscription;
+
+ bool get _isPlaying => _playerState == PlayerState.playing;
+
+ @override
+ void initState() {
+ super.initState();
+ _player = AudioPlayer();
+ _player.setReleaseMode(ReleaseMode.stop);
+ _initStreams();
+ }
+
+ @override
+ void dispose() {
+ _durationSubscription?.cancel();
+ _positionSubscription?.cancel();
+ _playerCompleteSubscription?.cancel();
+ _playerStateChangeSubscription?.cancel();
+ _player.dispose();
+ super.dispose();
+ }
+
+ Future _togglePlayPause() async {
+ try {
+ if (_isPlaying) {
+ await _player.pause();
+ } else {
+ await _player.setSource(AssetSource(widget.message.audioFile));
+ await _player.resume();
+ }
+ } on AudioPlayerException catch (e) {
+ _showErrorDialog('Audio Player Error', e.toString());
+ } catch (e) {
+ _showErrorDialog('Error', 'An unexpected error occurred. $e');
+ }
+ }
+
+ void _showErrorDialog(String title, String message) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: Text(title),
+ content: Text(message),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: const Text('OK'),
+ ),
+ ],
+ ),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ClipRRect(
+ borderRadius: widget.borderRadius ?? BorderRadius.zero,
+ child: Container(
+ constraints: widget.constraints,
+ color: Colors.grey[200],
+ child: Row(
+ children: [
+ IconButton(
+ icon: Icon(
+ _isPlaying ? Icons.pause : Icons.play_arrow,
+ size: 32,
+ ),
+ onPressed: _togglePlayPause,
+ ),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Slider(
+ value: _currentPosition?.inSeconds.toDouble() ?? 0.0,
+ max: _totalDuration?.inSeconds.toDouble() ?? 1.0,
+ onChanged: (value) async {
+ final position = Duration(seconds: value.toInt());
+ await _player.seek(position);
+ setState(() {
+ _currentPosition = position;
+ });
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _initStreams() {
+ _durationSubscription = _player.onDurationChanged.listen((duration) {
+ setState(() => _totalDuration = duration);
+ });
+
+ _positionSubscription = _player.onPositionChanged.listen((position) {
+ setState(() => _currentPosition = position);
+ });
+
+ _playerCompleteSubscription = _player.onPlayerComplete.listen((event) {
+ setState(() {
+ _playerState = PlayerState.stopped;
+ _currentPosition = Duration.zero;
+ });
+ });
+
+ _playerStateChangeSubscription = _player.onPlayerStateChanged.listen((state) {
+ setState(() {
+ _playerState = state;
+ });
+ });
+ }
+}
diff --git a/packages/flyer_chat_audio_message/pubspec.yaml b/packages/flyer_chat_audio_message/pubspec.yaml
index fdac7885..4b5119a5 100644
--- a/packages/flyer_chat_audio_message/pubspec.yaml
+++ b/packages/flyer_chat_audio_message/pubspec.yaml
@@ -10,8 +10,10 @@ environment:
flutter: ">=3.16.0"
dependencies:
+ audioplayers: ^6.1.0
flutter:
sdk: flutter
+ flutter_chat_core: ^0.0.2
dev_dependencies:
flutter_lints: ^4.0.0
diff --git a/packages/flyer_chat_custom_message/lib/src/flyer_chat_custom_message.dart b/packages/flyer_chat_custom_message/lib/src/flyer_chat_custom_message.dart
index e69de29b..8b137891 100644
--- a/packages/flyer_chat_custom_message/lib/src/flyer_chat_custom_message.dart
+++ b/packages/flyer_chat_custom_message/lib/src/flyer_chat_custom_message.dart
@@ -0,0 +1 @@
+
diff --git a/packages/flyer_chat_file_message/lib/src/flyer_chat_file_message.dart b/packages/flyer_chat_file_message/lib/src/flyer_chat_file_message.dart
index e69de29b..8b137891 100644
--- a/packages/flyer_chat_file_message/lib/src/flyer_chat_file_message.dart
+++ b/packages/flyer_chat_file_message/lib/src/flyer_chat_file_message.dart
@@ -0,0 +1 @@
+
diff --git a/packages/flyer_chat_image_message/lib/src/flyer_chat_image_message.dart b/packages/flyer_chat_image_message/lib/src/flyer_chat_image_message.dart
index 24d6610b..67f0d33e 100644
--- a/packages/flyer_chat_image_message/lib/src/flyer_chat_image_message.dart
+++ b/packages/flyer_chat_image_message/lib/src/flyer_chat_image_message.dart
@@ -6,8 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:image/image.dart' show encodeJpg;
import 'package:provider/provider.dart';
-import 'package:thumbhash/thumbhash.dart'
- show rgbaToBmp, thumbHashToApproximateAspectRatio, thumbHashToRGBA;
+import 'package:thumbhash/thumbhash.dart' show rgbaToBmp, thumbHashToApproximateAspectRatio, thumbHashToRGBA;
import 'custom_network_image.dart';
import 'preload_image_provider.dart';
@@ -28,8 +27,7 @@ class FlyerChatImageMessage extends StatefulWidget {
FlyerChatImageMessageState createState() => FlyerChatImageMessageState();
}
-class FlyerChatImageMessageState extends State
- with TickerProviderStateMixin {
+class FlyerChatImageMessageState extends State with TickerProviderStateMixin {
late ChatController _chatController;
late CustomNetworkImage _customNetworkImage;
late double _aspectRatio;
@@ -42,8 +40,7 @@ class FlyerChatImageMessageState extends State
if (widget.message.width != null && widget.message.height != null) {
_aspectRatio = widget.message.width! / widget.message.height!;
} else if (widget.message.thumbhash != null) {
- final thumbhashBytes =
- base64.decode(base64.normalize(widget.message.thumbhash!));
+ final thumbhashBytes = base64.decode(base64.normalize(widget.message.thumbhash!));
_aspectRatio = thumbHashToApproximateAspectRatio(thumbhashBytes);
@@ -84,10 +81,8 @@ class FlyerChatImageMessageState extends State
@override
Widget build(BuildContext context) {
- final backgroundColor =
- context.select((ChatTheme theme) => theme.backgroundColor);
- final imagePlaceholderColor =
- context.select((ChatTheme theme) => theme.imagePlaceholderColor);
+ final backgroundColor = context.select((ChatTheme theme) => theme.backgroundColor);
+ final imagePlaceholderColor = context.select((ChatTheme theme) => theme.imagePlaceholderColor);
return ClipRRect(
borderRadius: widget.borderRadius ?? BorderRadius.zero,
@@ -118,8 +113,7 @@ class FlyerChatImageMessageState extends State
child: CircularProgressIndicator(
color: backgroundColor.withOpacity(0.5),
value: loadingProgress.expectedTotalBytes != null
- ? loadingProgress.cumulativeBytesLoaded /
- loadingProgress.expectedTotalBytes!
+ ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: null,
),
);
diff --git a/packages/flyer_chat_location_message/lib/src/flyer_chat_location_message.dart b/packages/flyer_chat_location_message/lib/src/flyer_chat_location_message.dart
index e69de29b..8b137891 100644
--- a/packages/flyer_chat_location_message/lib/src/flyer_chat_location_message.dart
+++ b/packages/flyer_chat_location_message/lib/src/flyer_chat_location_message.dart
@@ -0,0 +1 @@
+
diff --git a/packages/flyer_chat_system_message/lib/src/flyer_chat_system_message.dart b/packages/flyer_chat_system_message/lib/src/flyer_chat_system_message.dart
index e69de29b..8b137891 100644
--- a/packages/flyer_chat_system_message/lib/src/flyer_chat_system_message.dart
+++ b/packages/flyer_chat_system_message/lib/src/flyer_chat_system_message.dart
@@ -0,0 +1 @@
+
diff --git a/packages/flyer_chat_video_message/lib/src/flyer_chat_video_message.dart b/packages/flyer_chat_video_message/lib/src/flyer_chat_video_message.dart
index e69de29b..8b137891 100644
--- a/packages/flyer_chat_video_message/lib/src/flyer_chat_video_message.dart
+++ b/packages/flyer_chat_video_message/lib/src/flyer_chat_video_message.dart
@@ -0,0 +1 @@
+