diff --git a/docs/qaul/flutter/windows.md b/docs/qaul/flutter/windows.md
index 1366edc82..e9f3f901c 100644
--- a/docs/qaul/flutter/windows.md
+++ b/docs/qaul/flutter/windows.md
@@ -3,11 +3,11 @@
## Prerequisits
### Install Visual Studio
-In order to develop for the windows desktop platform you need to have 'Microsoft Visual Studio 2019' or 'Microsoft Visual Studio 2022' installed.
+In order to develop for the windows desktop platform you need to have 'Microsoft Visual Studio 2022' installed.
-In Microsoft Visual Studio 2019/2022, you need to have 'Desktop development with C++' workload installed.
+In Microsoft Visual Studio 2022, you need to have 'Desktop development with C++' workload installed.
### Install Git for Windows
@@ -73,7 +73,7 @@ cargo build
# copy libqaul.dll to flutter runner
## this step is required in order to run flutter app
## create the location folder if it does not yet exist
-cp .\target\debug\libqaul.dll ..\qaul_ui\build\windows\runner\Debug\
+cp .\target\debug\libqaul.dll ..\qaul_ui\build\windows\x64\runner\Debug\
```
### Build and Run Windows Desktop App
diff --git a/qaul_ui/android/app/build.gradle b/qaul_ui/android/app/build.gradle
index 914204e25..e119e43d6 100644
--- a/qaul_ui/android/app/build.gradle
+++ b/qaul_ui/android/app/build.gradle
@@ -36,7 +36,7 @@ group = "net.qaul.app"
version = flutterVersionName
android {
- compileSdkVersion 33
+ compileSdkVersion 34
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
diff --git a/qaul_ui/android/app/src/main/AndroidManifest.xml b/qaul_ui/android/app/src/main/AndroidManifest.xml
index 199e74672..9e5a55034 100644
--- a/qaul_ui/android/app/src/main/AndroidManifest.xml
+++ b/qaul_ui/android/app/src/main/AndroidManifest.xml
@@ -38,6 +38,11 @@
+
+
+
+
+
diff --git a/qaul_ui/ios/Podfile.lock b/qaul_ui/ios/Podfile.lock
index 81dbb16d6..ec226b35e 100644
--- a/qaul_ui/ios/Podfile.lock
+++ b/qaul_ui/ios/Podfile.lock
@@ -1,4 +1,6 @@
PODS:
+ - audioplayers_darwin (0.0.1):
+ - Flutter
- better_open_file (0.0.1):
- Flutter
- device_info_plus (0.0.1):
@@ -53,6 +55,9 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
+ - record_darwin (1.0.0):
+ - Flutter
+ - FlutterMacOS
- SDWebImage (5.15.2):
- SDWebImage/Core (= 5.15.2)
- SDWebImage/Core (5.15.2)
@@ -66,6 +71,7 @@ PODS:
- Flutter
DEPENDENCIES:
+ - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`)
- better_open_file (from `.symlinks/plugins/better_open_file/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
@@ -77,6 +83,7 @@ DEPENDENCIES:
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
+ - record_darwin (from `.symlinks/plugins/record_darwin/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- uni_links (from `.symlinks/plugins/uni_links/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@@ -89,6 +96,8 @@ SPEC REPOS:
- SwiftyGif
EXTERNAL SOURCES:
+ audioplayers_darwin:
+ :path: ".symlinks/plugins/audioplayers_darwin/ios"
better_open_file:
:path: ".symlinks/plugins/better_open_file/ios"
device_info_plus:
@@ -111,6 +120,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
+ record_darwin:
+ :path: ".symlinks/plugins/record_darwin/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
uni_links:
@@ -119,6 +130,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
+ audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40
better_open_file: 03cf320415d4d3f46b6e00adc4a567d76c1a399d
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
@@ -132,6 +144,7 @@ SPEC CHECKSUMS:
integration_test: 13825b8a9334a850581300559b8839134b124670
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
+ record_darwin: 1f6619f2abac4d1ca91d3eeab038c980d76f1517
SDWebImage: 8ab87d4b3e5cc4927bd47f78db6ceb0b94442577
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
@@ -140,4 +153,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
-COCOAPODS: 1.14.3
+COCOAPODS: 1.15.2
diff --git a/qaul_ui/ios/Runner/Info.plist b/qaul_ui/ios/Runner/Info.plist
index 4cc38798d..426846bae 100644
--- a/qaul_ui/ios/Runner/Info.plist
+++ b/qaul_ui/ios/Runner/Info.plist
@@ -66,5 +66,7 @@
You can send photos as attachments to your peers. For that, please grant access to the application.
NSCameraUsageDescription
You can take pictures and send them as attachments to your peers. For that, please grant access to the application.
+ NSMicrophoneUsageDescription
+ You can send audio messages to your peers. For that, please grant access to the application.
diff --git a/qaul_ui/lib/qaul_app.dart b/qaul_ui/lib/qaul_app.dart
index f0d05029a..9b3bc8783 100644
--- a/qaul_ui/lib/qaul_app.dart
+++ b/qaul_ui/lib/qaul_app.dart
@@ -21,6 +21,10 @@ class QaulApp extends PlatformAwareBuilder {
return AdaptiveTheme(
light: ThemeData(
brightness: Brightness.light,
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: Colors.lightBlue,
+ brightness: Brightness.light,
+ ),
primarySwatch: Colors.lightBlue,
scaffoldBackgroundColor: Colors.white,
navigationBarTheme: const NavigationBarThemeData(
@@ -70,6 +74,10 @@ class QaulApp extends PlatformAwareBuilder {
),
dark: ThemeData(
brightness: Brightness.dark,
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: Colors.lightBlue,
+ brightness: Brightness.dark,
+ ),
primarySwatch: Colors.lightBlue,
visualDensity: VisualDensity.adaptivePlatformDensity,
iconTheme: const IconThemeData(color: Colors.white),
diff --git a/qaul_ui/lib/screens/home/tabs/chat/widgets/audio_message_widget.dart b/qaul_ui/lib/screens/home/tabs/chat/widgets/audio_message_widget.dart
new file mode 100644
index 000000000..f1f56e6ec
--- /dev/null
+++ b/qaul_ui/lib/screens/home/tabs/chat/widgets/audio_message_widget.dart
@@ -0,0 +1,182 @@
+part of 'chat.dart';
+
+class AudioMessageWidget extends StatefulWidget {
+ const AudioMessageWidget({
+ Key? key,
+ required this.message,
+ required this.messageWidth,
+ this.isDefaultUser = false,
+ }) : super(key: key);
+
+ final types.AudioMessage message;
+
+ final int messageWidth;
+
+ final bool isDefaultUser;
+
+ @override
+ State createState() => _AudioMessageWidgetState();
+}
+
+class _AudioMessageWidgetState extends State {
+ double get _controlSize => (widget.messageWidth.toDouble()) / 10;
+
+ final _audioPlayer = AudioPlayer()..setReleaseMode(ReleaseMode.stop);
+
+ Duration? _position;
+
+ Duration? _duration;
+
+ String? audioPath;
+
+ late StreamSubscription _playerStateChangedSubscription;
+
+ late StreamSubscription _durationChangedSubscription;
+
+ late StreamSubscription _positionChangedSubscription;
+
+ Color get primaryColor => Theme.of(context).colorScheme.primary;
+
+ Color get containerColor => Theme.of(context).colorScheme.primaryContainer;
+
+ Color get backgroundColor => Theme.of(context).colorScheme.background;
+
+ @override
+ void initState() {
+ _playerStateChangedSubscription =
+ _audioPlayer.onPlayerComplete.listen((state) async {
+ await stop();
+ });
+ _positionChangedSubscription = _audioPlayer.onPositionChanged.listen(
+ (position) => setState(() {
+ _position = position;
+ }),
+ );
+ _durationChangedSubscription = _audioPlayer.onDurationChanged.listen(
+ (duration) => setState(() {
+ _duration = duration;
+ }),
+ );
+
+ audioPath = widget.message.uri;
+ _getDuration();
+ _audioPlayer.setSource(_source);
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ _playerStateChangedSubscription.cancel();
+ _positionChangedSubscription.cancel();
+ _durationChangedSubscription.cancel();
+ _audioPlayer.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final ttheme = Theme.of(context).textTheme;
+
+ return Container(
+ padding: const EdgeInsetsDirectional.fromSTEB(16, 4, 8, 8),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SizedBox(height: _controlSize / 2),
+ Row(
+ mainAxisSize: MainAxisSize.max,
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ audioControls(),
+ Expanded(child: audioSlider()),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsetsDirectional.only(end: 16),
+ child: Text(
+ '${_duration?.inSeconds ?? 0.0} Seconds',
+ style: ttheme.labelLarge?.copyWith(
+ color: backgroundColor,
+ fontStyle: FontStyle.italic,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget audioControls() {
+ return ClipOval(
+ child: Material(
+ color: containerColor,
+ child: InkWell(
+ child: SizedBox(
+ width: _controlSize,
+ height: _controlSize,
+ child: Icon(
+ _audioPlayer.state == PlayerState.playing
+ ? Icons.pause
+ : Icons.play_arrow,
+ color: primaryColor,
+ ),
+ ),
+ onTap: () {
+ if (_audioPlayer.state == PlayerState.playing) {
+ pause();
+ } else {
+ play();
+ }
+ },
+ ),
+ ),
+ );
+ }
+
+ Widget audioSlider() {
+ bool canSetValue = false;
+ final duration = _duration;
+ final position = _position;
+
+ if (duration != null && position != null) {
+ canSetValue = position.inMilliseconds >= 0;
+ canSetValue &= position.inMilliseconds < duration.inMilliseconds;
+ }
+
+ return Slider(
+ activeColor: primaryColor,
+ inactiveColor: backgroundColor,
+ onChanged: (v) {
+ if (duration != null) {
+ final position = v * duration.inMilliseconds;
+ _audioPlayer.seek(Duration(milliseconds: position.round()));
+ }
+ },
+ value: canSetValue && duration != null && position != null
+ ? position.inMilliseconds / duration.inMilliseconds
+ : 0.0,
+ );
+ }
+
+ Future play() => _audioPlayer.play(_source);
+
+ Future pause() async {
+ await _audioPlayer.pause();
+ setState(() {});
+ }
+
+ Future stop() async {
+ await _audioPlayer.stop();
+ setState(() {});
+ }
+
+ Future _getDuration() async {
+ await _audioPlayer.setSourceUrl(audioPath!);
+ final duration = await _audioPlayer.getDuration();
+ _duration = duration;
+ }
+
+ Source get _source => DeviceFileSource(audioPath!);
+}
diff --git a/qaul_ui/lib/screens/home/tabs/chat/widgets/audio_recording.dart b/qaul_ui/lib/screens/home/tabs/chat/widgets/audio_recording.dart
new file mode 100644
index 000000000..13d84a27f
--- /dev/null
+++ b/qaul_ui/lib/screens/home/tabs/chat/widgets/audio_recording.dart
@@ -0,0 +1,208 @@
+part of 'chat.dart';
+
+class _RecordAudioDialog extends StatefulHookConsumerWidget {
+ const _RecordAudioDialog({
+ Key? key,
+ required this.room,
+ required this.onSendPressed,
+ this.partialMessage,
+ }) : super(key: key);
+ final ChatRoom room;
+ final String? partialMessage;
+ final Function(File f, types.PartialText desc) onSendPressed;
+
+ @override
+ ConsumerState<_RecordAudioDialog> createState() => _RecordAudioDialogState();
+}
+
+class _RecordAudioDialogState extends ConsumerState<_RecordAudioDialog> {
+ final audioPlayer = AudioPlayer();
+ final audioRecorder = AudioRecorder();
+
+ bool isRecording = false;
+ File? file;
+ String? audioPath;
+ late ComplexTimer _timer;
+ int _duration = 0;
+
+ void incrementDuration(ComplexTimer _) {
+ setState(() => _duration += 1);
+ _timer.restart();
+ }
+
+ void onStopPressed() async {
+ _timer.pause();
+ await stopRecording();
+ if (audioPath != null) {
+ setState(() => file = File(audioPath!));
+ }
+ }
+
+ void onCancelPressed() async {
+ if (isRecording) {
+ await stopRecording();
+ }
+ file?.deleteSync();
+ if (mounted) Navigator.pop(context);
+ }
+
+ void onSendPressed() {
+ final worker = ref.read(qaulWorkerProvider);
+ worker.sendFile(
+ pathName: file!.path,
+ conversationId: widget.room.conversationId,
+ description: 'audio message');
+
+ Navigator.pop(context);
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _timer = ComplexTimer(const Duration(seconds: 1));
+ _timer.onTimeout = incrementDuration;
+ _timer.pause();
+
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ startRecording();
+ _timer.restart();
+ });
+ }
+
+ @override
+ void dispose() {
+ _timer.cancel();
+ audioPlayer.dispose();
+ audioRecorder.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(8),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(12),
+ child: IconButtonFactory.close(onPressed: onCancelPressed),
+ ),
+ if (isRecording)
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.mic_rounded,
+ size: 28,
+ color: Colors.redAccent.shade100,
+ ),
+ Text(
+ _messageDuration,
+ ),
+ ],
+ ),
+ IconButton(
+ iconSize: 48,
+ onPressed: onStopPressed,
+ color: Colors.redAccent.shade100,
+ icon: const Icon(Icons.stop_circle_outlined),
+ ),
+ ],
+ ),
+ if (file != null) ...[
+ Row(
+ children: [
+ const SizedBox(width: 20),
+ const Icon(Icons.insert_drive_file_outlined, size: 40),
+ const SizedBox(width: 8),
+ Flexible(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(filesize(file!.lengthSync())),
+ Text(
+ _messageDuration,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.only(top: 20),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ OutlinedButton(
+ onPressed: onCancelPressed,
+ child: const Text('Cancel'),
+ ),
+ FilledButton(
+ onPressed: onSendPressed,
+ child: const Text('Send'),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ],
+ ),
+ );
+ }
+
+ String get _messageDuration => Duration(seconds: _duration)
+ .toString()
+ .substring(2)
+ .replaceAll('.000000', '');
+
+ Future startRecording() async {
+ try {
+ if (await audioRecorder.hasPermission()) {
+ setState(() {
+ isRecording = true;
+ });
+ final path = await getNewAudioFilePath();
+ await audioRecorder.start(const RecordConfig(), path: path);
+ }
+ } catch (e) {
+ return;
+ }
+ }
+
+ Future stopRecording() async {
+ try {
+ final path = await audioRecorder.stop();
+ setState(() {
+ isRecording = false;
+ audioPath = path!;
+ });
+ } catch (e) {
+ return;
+ }
+ }
+
+ Future getNewAudioFilePath() async {
+ final dir = (Platform.isAndroid)
+ ? (await getExternalStorageDirectory())
+ : (await getApplicationSupportDirectory());
+
+ if (dir == null) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(
+ content: Text(AppLocalizations.of(context)!.genericErrorMessage),
+ ));
+ }
+ return "";
+ }
+
+ return join(dir.path, 'audio_${DateTime.now().millisecondsSinceEpoch}.m4a');
+ }
+}
diff --git a/qaul_ui/lib/screens/home/tabs/chat/widgets/chat.dart b/qaul_ui/lib/screens/home/tabs/chat/widgets/chat.dart
index 79579756e..5131f8d0b 100644
--- a/qaul_ui/lib/screens/home/tabs/chat/widgets/chat.dart
+++ b/qaul_ui/lib/screens/home/tabs/chat/widgets/chat.dart
@@ -1,5 +1,7 @@
+import 'dart:async';
import 'dart:io';
+import 'package:audioplayers/audioplayers.dart';
import 'package:better_open_file/better_open_file.dart';
import 'package:bubble/bubble.dart';
import 'package:collection/collection.dart';
@@ -22,7 +24,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart' hide context, Context;
+import 'package:path_provider/path_provider.dart';
import 'package:qaul_rpc/qaul_rpc.dart';
+import 'package:record/record.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:utils/utils.dart';
@@ -33,6 +37,10 @@ import '../../../../../widgets/widgets.dart';
import '../../tab.dart';
import 'conditional/conditional.dart';
+part 'audio_message_widget.dart';
+
+part 'audio_recording.dart';
+
part 'custom_input.dart';
part 'file_message_widget.dart';
@@ -227,7 +235,8 @@ class _ChatScreenState extends ConsumerState {
sendButtonVisibilityMode: SendButtonVisibilityMode.always,
),
avatarBuilder: (id) {
- var user = room.members.firstWhereOrNull((u) => id.id == u.idBase58);
+ var user =
+ room.members.firstWhereOrNull((u) => id.id == u.idBase58);
if (user == null) return const SizedBox();
return QaulAvatar.small(user: user, badgeEnabled: false);
},
@@ -311,6 +320,37 @@ class _ChatScreenState extends ConsumerState {
);
}
},
+ onSendAudioPressed: !(Platform.isAndroid ||
+ Platform.isIOS ||
+ Platform.isMacOS ||
+ Platform.isWindows ||
+ Platform.isLinux)
+ ? null
+ : (room.messages?.isEmpty ?? true)
+ ? null
+ : ({types.PartialText? text}) async {
+ // ignore: use_build_context_synchronously
+ if (!context.mounted) return;
+ showModalBottomSheet(
+ context: context,
+ enableDrag: false,
+ isDismissible: false,
+ builder: (_) {
+ return _RecordAudioDialog(
+ room: room,
+ partialMessage: text?.text,
+ onSendPressed: (file, description) {
+ final worker = ref.read(qaulWorkerProvider);
+ worker.sendFile(
+ pathName: file.path,
+ conversationId: room.conversationId,
+ description: description.text,
+ );
+ },
+ );
+ },
+ );
+ },
),
onMessageTap: (context, message) async {
if (message is! types.FileMessage || _isReceivingFile(message)) {
@@ -386,6 +426,13 @@ class _ChatScreenState extends ConsumerState {
isDefaultUser: message.author.id == user.idBase58,
);
},
+ audioMessageBuilder: (message, {required int messageWidth}) {
+ return AudioMessageWidget(
+ message: message,
+ messageWidth: messageWidth,
+ isDefaultUser: message.author.id == user.idBase58,
+ );
+ },
theme: DefaultChatTheme(
userAvatarNameColors: [
colorGenerationStrategy(otherUser?.idBase58 ?? room.idBase58),
@@ -528,6 +575,21 @@ extension _MessageExtension on Message {
'messageState': status.toJson(),
},
);
+ } else if (mimeStr != null && RegExp('audio/.*').hasMatch(mimeStr)) {
+ return types.AudioMessage(
+ id: messageIdBase58,
+ duration: const Duration(seconds: 100),
+ author: author.toInternalUser(),
+ createdAt: receivedAt.millisecondsSinceEpoch,
+ status: mappedState,
+ uri: filePath,
+ size: (content as FileShareContent).size,
+ name: (content as FileShareContent).fileName,
+ metadata: {
+ 'description': (content as FileShareContent).description,
+ 'messageState': status.toJson(),
+ },
+ );
}
return types.FileMessage(
diff --git a/qaul_ui/lib/screens/home/tabs/chat/widgets/custom_input.dart b/qaul_ui/lib/screens/home/tabs/chat/widgets/custom_input.dart
index ddaa27c6b..471bb94d9 100644
--- a/qaul_ui/lib/screens/home/tabs/chat/widgets/custom_input.dart
+++ b/qaul_ui/lib/screens/home/tabs/chat/widgets/custom_input.dart
@@ -21,6 +21,7 @@ class _CustomInput extends StatefulWidget {
required this.hintText,
this.onAttachmentPressed,
this.onPickImagePressed,
+ this.onSendAudioPressed,
this.initialText,
this.disabledMessage,
this.isDisabled = false,
@@ -32,6 +33,8 @@ class _CustomInput extends StatefulWidget {
final Function({types.PartialText? text})? onPickImagePressed;
+ final Function({types.PartialText? text})? onSendAudioPressed;
+
final SendButtonVisibilityMode sendButtonVisibilityMode;
final String? initialText;
@@ -179,6 +182,11 @@ class _CustomInputState extends State<_CustomInput> {
onPressed: () => _sendFilePressed(
widget.onPickImagePressed),
),
+ if (widget.onSendAudioPressed != null)
+ _AttachmentButton(
+ icon: Icons.mic_none,
+ onPressed: widget.onSendAudioPressed,
+ ),
],
),
),
diff --git a/qaul_ui/lib/screens/home/tabs/chat/widgets/file_sharing.dart b/qaul_ui/lib/screens/home/tabs/chat/widgets/file_sharing.dart
index 402031563..0c2262415 100644
--- a/qaul_ui/lib/screens/home/tabs/chat/widgets/file_sharing.dart
+++ b/qaul_ui/lib/screens/home/tabs/chat/widgets/file_sharing.dart
@@ -47,8 +47,7 @@ class _SendFileDialog extends HookConsumerWidget {
const SizedBox(height: 8),
_CustomInput(
initialText: partialMessage,
- hintText: AppLocalizations.of(context)!
- .chatEmptyMessageHint,
+ hintText: AppLocalizations.of(context)!.chatEmptyMessageHint,
onSendPressed: (desc) {
final worker = ref.read(qaulWorkerProvider);
worker.sendFile(
diff --git a/qaul_ui/lib/utils.dart b/qaul_ui/lib/utils.dart
index 843f0ccaf..f7c14e485 100644
--- a/qaul_ui/lib/utils.dart
+++ b/qaul_ui/lib/utils.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
import 'package:fluent_ui/fluent_ui.dart';
class Responsiveness {
@@ -25,3 +27,123 @@ class Responsiveness {
static bool isDesktop(BuildContext context) =>
MediaQuery.of(context).size.width >= kDesktopBreakpoint;
}
+
+class ComplexTimer implements Timer {
+ final Zone _zone;
+ final Stopwatch _activeTimer = Stopwatch();
+ Timer? _timer;
+ final Duration _originalDuration;
+ void Function(ComplexTimer)? _onTimeout;
+ int _tick = 0;
+
+ ComplexTimer(Duration duration)
+ : _originalDuration = duration,
+ _zone = Zone.current {
+ _startTimer();
+ }
+
+ set onTimeout(void Function(ComplexTimer)? callback) {
+ _onTimeout = callback == null ? null : _zone.bindUnaryCallback(callback);
+ }
+
+ @override
+ int get tick => _tick;
+
+ // Whether the timer is actively counting.
+ @override
+ bool get isActive => _timer?.isActive ?? false;
+
+ // Whether the timer is started, but not currently actively counting.
+ bool get isPaused {
+ var timer = _timer;
+ return timer != null && !timer.isActive;
+ }
+
+ // Whether the timer has expired.
+ bool get isExpired => _timer == null;
+
+ /// Pauses an active timer.
+ ///
+ /// Nothing happens if the timer is already paused or expired.
+ void pause() {
+ _pauseTimer();
+ }
+
+ /// Resumes counting when paused.
+ ///
+ /// Nothing happens if the timer is active or expired.
+ void resume() {
+ var timer = _timer;
+ if (timer == null || timer.isActive) return;
+ _startTimer();
+ }
+
+ /// Resets the timer.
+ ///
+ /// Sets the timer to its original duration.
+ /// Does not change whether the timer is active, paused or expired.
+ void reset() {
+ var timer = _timer;
+ if (timer == null) return; // is expired.
+ _activeTimer
+ ..stop()
+ ..reset();
+ if (timer.isActive) {
+ timer.cancel();
+ _startTimer();
+ }
+ }
+
+ /// Restarts the timer.
+ ///
+ /// Starts counting for the original duration.
+ /// Works whether the timer is active, paused or expired.
+ void restart() {
+ _timer?.cancel();
+ _activeTimer
+ ..stop()
+ ..reset();
+ _startTimer();
+ }
+
+ /// Stops and expires the current timer.
+ ///
+ /// Nothing happens if the timer is already expired.
+ @override
+ void cancel() {
+ _timer?.cancel();
+ _expireTimer();
+ }
+
+ void _startTimer() {
+ var elapsed = _activeTimer.elapsedMilliseconds;
+ var duration = _originalDuration;
+ if (elapsed > 0) {
+ duration =
+ Duration(milliseconds: _originalDuration.inMilliseconds - elapsed);
+ }
+ _timer = _zone.createTimer(duration, _onTick);
+ _activeTimer.start();
+ }
+
+ void _expireTimer() {
+ _timer = null;
+ _activeTimer
+ ..stop()
+ ..reset();
+ }
+
+ void _pauseTimer() {
+ _timer?.cancel();
+ _activeTimer.stop();
+ }
+
+ void _onTick() {
+ _tick++;
+ _expireTimer();
+ var callback = _onTimeout;
+ if (callback != null) {
+ _zone.runUnary(callback, this);
+ }
+ }
+}
diff --git a/qaul_ui/macos/Podfile b/qaul_ui/macos/Podfile
index 049abe295..9ec46f8cd 100644
--- a/qaul_ui/macos/Podfile
+++ b/qaul_ui/macos/Podfile
@@ -1,4 +1,4 @@
-platform :osx, '10.14'
+platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/qaul_ui/macos/Podfile.lock b/qaul_ui/macos/Podfile.lock
index 7045780c6..edb3b8c95 100644
--- a/qaul_ui/macos/Podfile.lock
+++ b/qaul_ui/macos/Podfile.lock
@@ -1,4 +1,6 @@
PODS:
+ - audioplayers_darwin (0.0.1):
+ - FlutterMacOS
- device_info_plus (0.0.1):
- FlutterMacOS
- file_selector_macos (0.0.1):
@@ -13,6 +15,9 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
+ - record_darwin (1.0.0):
+ - Flutter
+ - FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -20,6 +25,7 @@ PODS:
- FlutterMacOS
DEPENDENCIES:
+ - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- flutter_app_badger (from `Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos`)
@@ -27,10 +33,13 @@ DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
+ - record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES:
+ audioplayers_darwin:
+ :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos
device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
file_selector_macos:
@@ -45,12 +54,15 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
+ record_darwin:
+ :path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
+ audioplayers_darwin: dcad41de4fbd0099cb3749f7ab3b0cb8f70b810c
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9
flutter_app_badger: 55a64b179f8438e89d574320c77b306e327a1730
@@ -58,9 +70,10 @@ SPEC CHECKSUMS:
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
+ record_darwin: 1f6619f2abac4d1ca91d3eeab038c980d76f1517
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
-PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
+PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0
COCOAPODS: 1.15.2
diff --git a/qaul_ui/macos/Runner.xcodeproj/project.pbxproj b/qaul_ui/macos/Runner.xcodeproj/project.pbxproj
index bd1a7f49f..eb4e60bd0 100644
--- a/qaul_ui/macos/Runner.xcodeproj/project.pbxproj
+++ b/qaul_ui/macos/Runner.xcodeproj/project.pbxproj
@@ -209,7 +209,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
- LastUpgradeCheck = 1300;
+ LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
33CC10EC2044A3C60003C045 = {
@@ -440,8 +440,8 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
- MACOSX_DEPLOYMENT_TARGET = 10.14;
- MARKETING_VERSION = 2.0.0-beta.17;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MARKETING_VERSION = "2.0.0-beta.17";
PRODUCT_BUNDLE_IDENTIFIER = net.qaul.app;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
@@ -576,8 +576,8 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
- MACOSX_DEPLOYMENT_TARGET = 10.14;
- MARKETING_VERSION = 2.0.0-beta.17;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MARKETING_VERSION = "2.0.0-beta.17";
PRODUCT_BUNDLE_IDENTIFIER = net.qaul.app;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -607,8 +607,8 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
- MACOSX_DEPLOYMENT_TARGET = 10.14;
- MARKETING_VERSION = 2.0.0-beta.17;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
+ MARKETING_VERSION = "2.0.0-beta.17";
PRODUCT_BUNDLE_IDENTIFIER = net.qaul.app;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
diff --git a/qaul_ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/qaul_ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 9e1de3a16..016d8f6de 100644
--- a/qaul_ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/qaul_ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
com.apple.security.cs.allow-jit
- com.apple.security.network.server
+ com.apple.security.device.audio-input
com.apple.security.network.client
-
+
+ com.apple.security.network.server
+
diff --git a/qaul_ui/macos/Runner/Info.plist b/qaul_ui/macos/Runner/Info.plist
index e5bc2a248..350a87483 100644
--- a/qaul_ui/macos/Runner/Info.plist
+++ b/qaul_ui/macos/Runner/Info.plist
@@ -30,5 +30,7 @@
MainMenu
NSPrincipalClass
NSApplication
+ NSMicrophoneUsageDescription
+ You can send audio messages to your peers. For that, please grant access to the application.
diff --git a/qaul_ui/macos/Runner/Release.entitlements b/qaul_ui/macos/Runner/Release.entitlements
index 7a2230dc3..9f3c0e8c3 100644
--- a/qaul_ui/macos/Runner/Release.entitlements
+++ b/qaul_ui/macos/Runner/Release.entitlements
@@ -4,6 +4,8 @@
com.apple.security.app-sandbox
+ com.apple.security.device.audio-input
+
com.apple.security.network.client
com.apple.security.network.server
diff --git a/qaul_ui/packages/qaul_rpc/lib/src/utils.dart b/qaul_ui/packages/qaul_rpc/lib/src/utils.dart
index 77ac20162..0a4813e40 100644
--- a/qaul_ui/packages/qaul_rpc/lib/src/utils.dart
+++ b/qaul_ui/packages/qaul_rpc/lib/src/utils.dart
@@ -26,6 +26,9 @@ abstract class FilePathResolverMixin {
required String extension,
}) {
var storagePath = read(libqaulLogsStoragePath)!.replaceAll('/logs', '');
+ if (Platform.isWindows){
+ storagePath = read(libqaulLogsStoragePath)!.replaceAll('\\logs', '');
+ }
var uuid = read(defaultUserProvider)!.idBase58;
return '$storagePath/$uuid/files/$id.$extension';
diff --git a/qaul_ui/pubspec.lock b/qaul_ui/pubspec.lock
index bda21da1a..a4912557d 100644
--- a/qaul_ui/pubspec.lock
+++ b/qaul_ui/pubspec.lock
@@ -49,6 +49,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
+ audioplayers:
+ dependency: "direct main"
+ description:
+ name: audioplayers
+ sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.2.1"
+ audioplayers_android:
+ dependency: transitive
+ description:
+ name: audioplayers_android
+ sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.3"
+ audioplayers_darwin:
+ dependency: transitive
+ description:
+ name: audioplayers_darwin
+ sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.2"
+ audioplayers_linux:
+ dependency: transitive
+ description:
+ name: audioplayers_linux
+ sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.0"
+ audioplayers_platform_interface:
+ dependency: transitive
+ description:
+ name: audioplayers_platform_interface
+ sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.1.0"
+ audioplayers_web:
+ dependency: transitive
+ description:
+ name: audioplayers_web
+ sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.1.0"
+ audioplayers_windows:
+ dependency: transitive
+ description:
+ name: audioplayers_windows
+ sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.0"
badges:
dependency: "direct main"
description:
@@ -1106,6 +1162,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
+ record:
+ dependency: "direct main"
+ description:
+ name: record
+ sha256: "5c8e12c692a4800b33f5f8b6c821ea083b12bfdbd031b36ba9322c40a4eeecc9"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.4"
+ record_android:
+ dependency: transitive
+ description:
+ name: record_android
+ sha256: "805ecaa232a671aff2ee9ec4730ef6addb97c548d2db6b1fbd5197f1d4f47a5a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
+ record_darwin:
+ dependency: transitive
+ description:
+ name: record_darwin
+ sha256: ee8cb1bb1712d7ce38140ecabe70e5c286c02f05296d66043bee865ace7eb1b9
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
+ record_linux:
+ dependency: transitive
+ description:
+ name: record_linux
+ sha256: "7d0e70cd51635128fe9d37d89bafd6011d7cbba9af8dc323079ae60f23546aef"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.1"
+ record_platform_interface:
+ dependency: transitive
+ description:
+ name: record_platform_interface
+ sha256: "3a4b56e94ecd2a0b2b43eb1fa6f94c5b8484334f5d38ef43959c4bf97fb374cf"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.2"
+ record_web:
+ dependency: transitive
+ description:
+ name: record_web
+ sha256: "24847cdbcf999f7a5762170792f622ac844858766becd0f2370ec8ae22f7526e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.5"
+ record_windows:
+ dependency: transitive
+ description:
+ name: record_windows
+ sha256: "39998b3ea7d8d28b04159d82220e6e5e32a7c357c6fb2794f5736beea272f6c3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.2"
responsive_framework:
dependency: "direct main"
description:
@@ -1319,6 +1431,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.1"
+ synchronized:
+ dependency: transitive
+ description:
+ name: synchronized
+ sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.0+1"
term_glyph:
dependency: transitive
description:
diff --git a/qaul_ui/pubspec.yaml b/qaul_ui/pubspec.yaml
index 17531db55..c9ff3125f 100644
--- a/qaul_ui/pubspec.yaml
+++ b/qaul_ui/pubspec.yaml
@@ -62,6 +62,8 @@ dependencies:
uni_links: ^0.5.1
url_launcher: ^6.1.14
utils: { "path": "packages/utils" }
+ record: ^5.0.4
+ audioplayers: ^5.2.1
dev_dependencies:
flutter_test: