diff --git a/lib/l10n/intl_be.arb b/lib/l10n/intl_be.arb index 438de60c4..26dca3a78 100644 --- a/lib/l10n/intl_be.arb +++ b/lib/l10n/intl_be.arb @@ -1,4 +1,3 @@ -{ { "alwaysUse24HourFormat": "адключана", "@alwaysUse24HourFormat": { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 091cbd7d9..2cc4e5ba1 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -3376,6 +3376,9 @@ "declineInvitation": "Decline invitation", "@declineInvitation": {}, "noMessagesYet": "No messages yet", + "longPressToRecordVoiceMessage": "Long press to record voice message.", + "pause": "Pause", + "resume": "Resume", "ignore": "Block", "ignoredUsers": "Blocked users", "writeAMessageLangCodes": "Type in {l1} or {l2}...", diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index ce14f3e24..298df8b0d 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -25,7 +25,6 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat_view.dart'; import 'package:fluffychat/pages/chat/event_info_dialog.dart'; import 'package:fluffychat/pages/chat/events/audio_player.dart'; -import 'package:fluffychat/pages/chat/recording_dialog.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_chat_controller.dart'; @@ -1140,13 +1139,14 @@ class ChatController extends State ); } - void voiceMessageAction() async { + Future onVoiceMessageSend( + String path, + int duration, + List waveform, + String? fileName, + ) async { // #Pangea stopMediaStream.add(null); - // Pangea# - room.client.getConfig(); // Preload server file configuration. - - final scaffoldMessenger = ScaffoldMessenger.of(context); if (PlatformInfos.isAndroid) { final info = await DeviceInfoPlugin().androidInfo; if (info.version.sdkInt < 19) { @@ -1159,17 +1159,9 @@ class ChatController extends State return; } } - - // #Pangea - // if (await AudioRecorder().hasPermission() == false) return; // Pangea# - final result = await showDialog( - context: context, - barrierDismissible: false, - builder: (c) => const RecordingDialog(), - ); - if (result == null) return; - final audioFile = XFile(result.path); + final scaffoldMessenger = ScaffoldMessenger.of(context); + final audioFile = XFile(path); final bytesResult = await showFutureLoadingDialog( context: context, @@ -1180,30 +1172,31 @@ class ChatController extends State final file = MatrixAudioFile( bytes: bytes, - name: result.fileName ?? audioFile.path, + name: fileName ?? audioFile.path, ); // #Pangea - final reply = replyEvent.value; + // setState(() { + // replyEvent = null; + // }); replyEvent.value = null; // Pangea# - - await room + room .sendFileEvent( file, // #Pangea // inReplyTo: replyEvent, - inReplyTo: reply, + inReplyTo: replyEvent.value, // Pangea# extraContent: { 'info': { ...file.info, - 'duration': result.duration, + 'duration': duration, }, 'org.matrix.msc3245.voice': {}, 'org.matrix.msc1767.audio': { - 'duration': result.duration, - 'waveform': result.waveform, + 'duration': duration, + 'waveform': waveform, }, // #Pangea 'speaker_l1': pangeaController.userController.userL1Code, @@ -1220,8 +1213,6 @@ class ChatController extends State data: { 'roomId': roomId, 'file': file.name, - 'duration': result.duration, - 'waveform': result.waveform, }, ); scaffoldMessenger.showSnackBar( @@ -1243,9 +1234,7 @@ class ChatController extends State // ); // return null; // }); - // setState(() { - // replyEvent = null; - // }); + // return; // Pangea# } diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 0e3c9d80f..8bdef7d09 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,14 +1,15 @@ -// import 'package:flutter/material.dart'; - // import 'package:animations/animations.dart'; -// import 'package:matrix/matrix.dart'; - // import 'package:fluffychat/config/app_config.dart'; // import 'package:fluffychat/l10n/l10n.dart'; +// import 'package:fluffychat/pages/chat/recording_input_row.dart'; +// import 'package:fluffychat/pages/chat/recording_view_model.dart'; // import 'package:fluffychat/utils/other_party_can_receive.dart'; // import 'package:fluffychat/utils/platform_infos.dart'; // import 'package:fluffychat/widgets/avatar.dart'; // import 'package:fluffychat/widgets/matrix.dart'; +// import 'package:flutter/material.dart'; +// import 'package:matrix/matrix.dart'; + // import '../../config/themes.dart'; // import 'chat.dart'; // import 'input_bar.dart'; @@ -41,297 +42,324 @@ // foregroundColor: theme.colorScheme.onTertiaryContainer, // ); -// return Row( -// crossAxisAlignment: CrossAxisAlignment.end, -// mainAxisAlignment: MainAxisAlignment.spaceBetween, -// children: controller.selectMode -// ? [ -// if (controller.selectedEvents -// .every((event) => event.status == EventStatus.error)) -// SizedBox( -// height: height, -// child: TextButton( -// style: TextButton.styleFrom( -// foregroundColor: theme.colorScheme.error, +// return RecordingViewModel( +// builder: (context, recordingViewModel) { +// if (recordingViewModel.isRecording) { +// return RecordingInputRow( +// state: recordingViewModel, +// onSend: controller.onVoiceMessageSend, +// ); +// } +// return Row( +// crossAxisAlignment: CrossAxisAlignment.end, +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: controller.selectMode +// ? [ +// if (controller.selectedEvents +// .every((event) => event.status == EventStatus.error)) +// SizedBox( +// height: height, +// child: TextButton( +// style: TextButton.styleFrom( +// foregroundColor: theme.colorScheme.error, +// ), +// onPressed: controller.deleteErrorEventsAction, +// child: Row( +// children: [ +// const Icon(Icons.delete_forever_outlined), +// Text(L10n.of(context).delete), +// ], +// ), +// ), +// ) +// else +// SizedBox( +// height: height, +// child: TextButton( +// style: selectedTextButtonStyle, +// onPressed: controller.forwardEventsAction, +// child: Row( +// children: [ +// const Icon(Icons.keyboard_arrow_left_outlined), +// Text(L10n.of(context).forward), +// ], +// ), +// ), // ), -// onPressed: controller.deleteErrorEventsAction, -// child: Row( -// children: [ -// const Icon(Icons.delete_forever_outlined), -// Text(L10n.of(context).delete), +// controller.selectedEvents.length == 1 +// ? controller.selectedEvents.first +// .getDisplayEvent(controller.timeline!) +// .status +// .isSent +// ? SizedBox( +// height: height, +// child: TextButton( +// style: selectedTextButtonStyle, +// onPressed: controller.replyAction, +// child: Row( +// children: [ +// Text(L10n.of(context).reply), +// const Icon(Icons.keyboard_arrow_right), +// ], +// ), +// ), +// ) +// : SizedBox( +// height: height, +// child: TextButton( +// style: selectedTextButtonStyle, +// onPressed: controller.sendAgainAction, +// child: Row( +// children: [ +// Text(L10n.of(context).tryToSendAgain), +// const SizedBox(width: 4), +// const Icon(Icons.send_outlined, size: 16), +// ], +// ), +// ), +// ) +// : const SizedBox.shrink(), +// ] +// : [ +// const SizedBox(width: 4), +// AnimatedContainer( +// duration: FluffyThemes.animationDuration, +// curve: FluffyThemes.animationCurve, +// width: +// controller.sendController.text.isNotEmpty ? 0 : height, +// height: height, +// alignment: Alignment.center, +// decoration: const BoxDecoration(), +// clipBehavior: Clip.hardEdge, +// child: PopupMenuButton( +// useRootNavigator: true, +// icon: const Icon(Icons.add_circle_outline), +// iconColor: theme.colorScheme.onPrimaryContainer, +// onSelected: controller.onAddPopupMenuButtonSelected, +// itemBuilder: (BuildContext context) => +// >[ +// if (PlatformInfos.isMobile) +// PopupMenuItem( +// value: 'location', +// child: ListTile( +// leading: CircleAvatar( +// backgroundColor: +// theme.colorScheme.onPrimaryContainer, +// foregroundColor: +// theme.colorScheme.primaryContainer, +// child: const Icon(Icons.gps_fixed_outlined), +// ), +// title: Text(L10n.of(context).shareLocation), +// contentPadding: const EdgeInsets.all(0), +// ), +// ), +// PopupMenuItem( +// value: 'image', +// child: ListTile( +// leading: CircleAvatar( +// backgroundColor: +// theme.colorScheme.onPrimaryContainer, +// foregroundColor: +// theme.colorScheme.primaryContainer, +// child: const Icon(Icons.photo_outlined), +// ), +// title: Text(L10n.of(context).sendImage), +// contentPadding: const EdgeInsets.all(0), +// ), +// ), +// PopupMenuItem( +// value: 'video', +// child: ListTile( +// leading: CircleAvatar( +// backgroundColor: +// theme.colorScheme.onPrimaryContainer, +// foregroundColor: +// theme.colorScheme.primaryContainer, +// child: +// const Icon(Icons.video_camera_back_outlined), +// ), +// title: Text(L10n.of(context).sendVideo), +// contentPadding: const EdgeInsets.all(0), +// ), +// ), +// PopupMenuItem( +// value: 'file', +// child: ListTile( +// leading: CircleAvatar( +// backgroundColor: +// theme.colorScheme.onPrimaryContainer, +// foregroundColor: +// theme.colorScheme.primaryContainer, +// child: const Icon(Icons.attachment_outlined), +// ), +// title: Text(L10n.of(context).sendFile), +// contentPadding: const EdgeInsets.all(0), +// ), +// ), // ], // ), // ), -// ) -// else -// SizedBox( -// height: height, -// child: TextButton( -// style: selectedTextButtonStyle, -// onPressed: controller.forwardEventsAction, -// child: Row( -// children: [ -// const Icon(Icons.keyboard_arrow_left_outlined), -// Text(L10n.of(context).forward), -// ], -// ), -// ), -// ), -// controller.selectedEvents.length == 1 -// ? controller.selectedEvents.first -// .getDisplayEvent(controller.timeline!) -// .status -// .isSent -// ? SizedBox( -// height: height, -// child: TextButton( -// style: selectedTextButtonStyle, -// onPressed: controller.replyAction, -// child: Row( -// children: [ -// Text(L10n.of(context).reply), -// const Icon(Icons.keyboard_arrow_right), -// ], +// if (PlatformInfos.isMobile) +// AnimatedContainer( +// duration: FluffyThemes.animationDuration, +// curve: FluffyThemes.animationCurve, +// width: controller.sendController.text.isNotEmpty +// ? 0 +// : height, +// height: height, +// alignment: Alignment.center, +// decoration: const BoxDecoration(), +// clipBehavior: Clip.hardEdge, +// child: PopupMenuButton( +// useRootNavigator: true, +// icon: const Icon(Icons.camera_alt_outlined), +// onSelected: controller.onAddPopupMenuButtonSelected, +// iconColor: theme.colorScheme.onPrimaryContainer, +// itemBuilder: (context) => [ +// PopupMenuItem( +// value: 'camera-video', +// child: ListTile( +// leading: CircleAvatar( +// backgroundColor: +// theme.colorScheme.onPrimaryContainer, +// foregroundColor: +// theme.colorScheme.primaryContainer, +// child: const Icon(Icons.videocam_outlined), +// ), +// title: Text(L10n.of(context).recordAVideo), +// contentPadding: const EdgeInsets.all(0), // ), // ), -// ) -// : SizedBox( -// height: height, -// child: TextButton( -// style: selectedTextButtonStyle, -// onPressed: controller.sendAgainAction, -// child: Row( -// children: [ -// Text(L10n.of(context).tryToSendAgain), -// const SizedBox(width: 4), -// const Icon(Icons.send_outlined, size: 16), -// ], +// PopupMenuItem( +// value: 'camera', +// child: ListTile( +// leading: CircleAvatar( +// backgroundColor: +// theme.colorScheme.onPrimaryContainer, +// foregroundColor: +// theme.colorScheme.primaryContainer, +// child: const Icon(Icons.camera_alt_outlined), +// ), +// title: Text(L10n.of(context).takeAPhoto), +// contentPadding: const EdgeInsets.all(0), // ), // ), -// ) -// : const SizedBox.shrink(), -// ] -// : [ -// const SizedBox(width: 4), -// AnimatedContainer( -// duration: FluffyThemes.animationDuration, -// curve: FluffyThemes.animationCurve, -// width: controller.sendController.text.isNotEmpty ? 0 : height, -// height: height, -// alignment: Alignment.center, -// decoration: const BoxDecoration(), -// clipBehavior: Clip.hardEdge, -// child: PopupMenuButton( -// useRootNavigator: true, -// icon: const Icon(Icons.add_circle_outline), -// iconColor: theme.colorScheme.onPrimaryContainer, -// onSelected: controller.onAddPopupMenuButtonSelected, -// itemBuilder: (BuildContext context) => -// >[ -// if (PlatformInfos.isMobile) -// PopupMenuItem( -// value: 'location', -// child: ListTile( -// leading: CircleAvatar( -// backgroundColor: -// theme.colorScheme.onPrimaryContainer, -// foregroundColor: theme.colorScheme.primaryContainer, -// child: const Icon(Icons.gps_fixed_outlined), -// ), -// title: Text(L10n.of(context).shareLocation), -// contentPadding: const EdgeInsets.all(0), -// ), -// ), -// PopupMenuItem( -// value: 'image', -// child: ListTile( -// leading: CircleAvatar( -// backgroundColor: theme.colorScheme.onPrimaryContainer, -// foregroundColor: theme.colorScheme.primaryContainer, -// child: const Icon(Icons.photo_outlined), -// ), -// title: Text(L10n.of(context).sendImage), -// contentPadding: const EdgeInsets.all(0), +// ], // ), // ), -// PopupMenuItem( -// value: 'video', -// child: ListTile( -// leading: CircleAvatar( -// backgroundColor: theme.colorScheme.onPrimaryContainer, -// foregroundColor: theme.colorScheme.primaryContainer, -// child: const Icon(Icons.video_camera_back_outlined), -// ), -// title: Text(L10n.of(context).sendVideo), -// contentPadding: const EdgeInsets.all(0), -// ), -// ), -// PopupMenuItem( -// value: 'file', -// child: ListTile( -// leading: CircleAvatar( -// backgroundColor: theme.colorScheme.onPrimaryContainer, -// foregroundColor: theme.colorScheme.primaryContainer, -// child: const Icon(Icons.attachment_outlined), -// ), -// title: Text(L10n.of(context).sendFile), -// contentPadding: const EdgeInsets.all(0), -// ), -// ), -// ], -// ), -// ), -// if (PlatformInfos.isMobile) -// AnimatedContainer( -// duration: FluffyThemes.animationDuration, -// curve: FluffyThemes.animationCurve, -// width: controller.sendController.text.isNotEmpty ? 0 : height, -// height: height, -// alignment: Alignment.center, -// decoration: const BoxDecoration(), -// clipBehavior: Clip.hardEdge, -// child: PopupMenuButton( -// useRootNavigator: true, -// icon: const Icon(Icons.camera_alt_outlined), -// onSelected: controller.onAddPopupMenuButtonSelected, -// iconColor: theme.colorScheme.onPrimaryContainer, -// itemBuilder: (context) => [ -// PopupMenuItem( -// value: 'camera-video', -// child: ListTile( -// leading: CircleAvatar( -// backgroundColor: -// theme.colorScheme.onPrimaryContainer, -// foregroundColor: theme.colorScheme.primaryContainer, -// child: const Icon(Icons.videocam_outlined), -// ), -// title: Text(L10n.of(context).recordAVideo), -// contentPadding: const EdgeInsets.all(0), +// Container( +// height: height, +// width: height, +// alignment: Alignment.center, +// child: IconButton( +// tooltip: L10n.of(context).emojis, +// color: theme.colorScheme.onPrimaryContainer, +// icon: PageTransitionSwitcher( +// transitionBuilder: ( +// Widget child, +// Animation primaryAnimation, +// Animation secondaryAnimation, +// ) { +// return SharedAxisTransition( +// animation: primaryAnimation, +// secondaryAnimation: secondaryAnimation, +// transitionType: SharedAxisTransitionType.scaled, +// fillColor: Colors.transparent, +// child: child, +// ); +// }, +// child: Icon( +// controller.showEmojiPicker +// ? Icons.keyboard +// : Icons.add_reaction_outlined, +// key: ValueKey(controller.showEmojiPicker), // ), // ), -// PopupMenuItem( -// value: 'camera', -// child: ListTile( -// leading: CircleAvatar( -// backgroundColor: -// theme.colorScheme.onPrimaryContainer, -// foregroundColor: theme.colorScheme.primaryContainer, -// child: const Icon(Icons.camera_alt_outlined), -// ), -// title: Text(L10n.of(context).takeAPhoto), -// contentPadding: const EdgeInsets.all(0), -// ), -// ), -// ], -// ), -// ), -// Container( -// height: height, -// width: height, -// alignment: Alignment.center, -// child: IconButton( -// tooltip: L10n.of(context).emojis, -// color: theme.colorScheme.onPrimaryContainer, -// icon: PageTransitionSwitcher( -// transitionBuilder: ( -// Widget child, -// Animation primaryAnimation, -// Animation secondaryAnimation, -// ) { -// return SharedAxisTransition( -// animation: primaryAnimation, -// secondaryAnimation: secondaryAnimation, -// transitionType: SharedAxisTransitionType.scaled, -// fillColor: Colors.transparent, -// child: child, -// ); -// }, -// child: Icon( -// controller.showEmojiPicker -// ? Icons.keyboard -// : Icons.add_reaction_outlined, -// key: ValueKey(controller.showEmojiPicker), +// onPressed: controller.emojiPickerAction, // ), // ), -// onPressed: controller.emojiPickerAction, -// ), -// ), -// if (Matrix.of(context).isMultiAccount && -// Matrix.of(context).hasComplexBundles && -// Matrix.of(context).currentBundle!.length > 1) -// Container( -// height: height, -// width: height, -// alignment: Alignment.center, -// child: _ChatAccountPicker(controller), -// ), -// Expanded( -// child: Padding( -// padding: const EdgeInsets.symmetric(vertical: 0.0), -// child: InputBar( -// room: controller.room, -// minLines: 1, -// maxLines: 8, -// autofocus: !PlatformInfos.isMobile, -// keyboardType: TextInputType.multiline, -// textInputAction: -// AppConfig.sendOnEnter == true && PlatformInfos.isMobile +// if (Matrix.of(context).isMultiAccount && +// Matrix.of(context).hasComplexBundles && +// Matrix.of(context).currentBundle!.length > 1) +// Container( +// height: height, +// width: height, +// alignment: Alignment.center, +// child: _ChatAccountPicker(controller), +// ), +// Expanded( +// child: Padding( +// padding: const EdgeInsets.symmetric(vertical: 0.0), +// child: InputBar( +// room: controller.room, +// minLines: 1, +// maxLines: 8, +// autofocus: !PlatformInfos.isMobile, +// keyboardType: TextInputType.multiline, +// textInputAction: AppConfig.sendOnEnter == true && +// PlatformInfos.isMobile // ? TextInputAction.send // : null, -// onSubmitted: controller.onInputBarSubmitted, -// onSubmitImage: controller.sendImageFromClipBoard, -// focusNode: controller.inputFocus, -// controller: controller.sendController, -// decoration: InputDecoration( -// contentPadding: const EdgeInsets.only( -// left: 6.0, -// right: 6.0, -// bottom: 6.0, -// top: 3.0, +// onSubmitted: controller.onInputBarSubmitted, +// onSubmitImage: controller.sendImageFromClipBoard, +// focusNode: controller.inputFocus, +// controller: controller.sendController, +// decoration: InputDecoration( +// contentPadding: const EdgeInsets.only( +// left: 6.0, +// right: 6.0, +// bottom: 6.0, +// top: 3.0, +// ), +// counter: const SizedBox.shrink(), +// hintText: L10n.of(context).writeAMessage, +// hintMaxLines: 1, +// border: InputBorder.none, +// enabledBorder: InputBorder.none, +// filled: false, +// ), +// onChanged: controller.onInputBarChanged, // ), -// counter: const SizedBox.shrink(), -// hintText: L10n.of(context).writeAMessage, -// hintMaxLines: 1, -// border: InputBorder.none, -// enabledBorder: InputBorder.none, -// filled: false, // ), -// onChanged: controller.onInputBarChanged, // ), -// ), -// ), -// Container( -// height: height, -// width: height, -// alignment: Alignment.center, -// child: PlatformInfos.platformCanRecord && -// controller.sendController.text.isEmpty -// ? FloatingActionButton.small( -// tooltip: L10n.of(context).voiceMessage, -// onPressed: controller.voiceMessageAction, -// elevation: 0, -// heroTag: null, -// shape: RoundedRectangleBorder( -// borderRadius: BorderRadius.circular(height), -// ), -// backgroundColor: theme.bubbleColor, -// foregroundColor: theme.onBubbleColor, -// child: const Icon(Icons.mic_none_outlined), -// ) -// : FloatingActionButton.small( -// tooltip: L10n.of(context).send, -// onPressed: controller.send, -// elevation: 0, -// heroTag: null, -// shape: RoundedRectangleBorder( -// borderRadius: BorderRadius.circular(height), -// ), -// backgroundColor: theme.bubbleColor, -// foregroundColor: theme.onBubbleColor, -// child: const Icon(Icons.send_outlined), -// ), -// ), -// ], +// Container( +// height: height, +// width: height, +// alignment: Alignment.center, +// child: PlatformInfos.platformCanRecord && +// controller.sendController.text.isEmpty +// ? IconButton( +// tooltip: L10n.of(context).voiceMessage, +// onPressed: () => +// ScaffoldMessenger.of(context).showSnackBar( +// SnackBar( +// content: Text( +// L10n.of(context) +// .longPressToRecordVoiceMessage, +// ), +// ), +// ), +// onLongPress: () => recordingViewModel +// .startRecording(controller.room), +// style: IconButton.styleFrom( +// backgroundColor: theme.bubbleColor, +// foregroundColor: theme.onBubbleColor, +// ), +// icon: const Icon(Icons.mic_none_outlined), +// ) +// : IconButton( +// tooltip: L10n.of(context).send, +// onPressed: controller.send, +// style: IconButton.styleFrom( +// backgroundColor: theme.bubbleColor, +// foregroundColor: theme.onBubbleColor, +// ), +// icon: const Icon(Icons.send_outlined), +// ), +// ), +// ], +// ); +// }, // ); // } // } diff --git a/lib/pages/chat/recording_dialog.dart b/lib/pages/chat/recording_dialog.dart deleted file mode 100644 index 501e20663..000000000 --- a/lib/pages/chat/recording_dialog.dart +++ /dev/null @@ -1,318 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'package:path/path.dart' as path_lib; -import 'package:path_provider/path_provider.dart'; -import 'package:record/record.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; - -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/config/setting_keys.dart'; -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'events/audio_player.dart'; - -class PermissionException implements Exception {} - -class EmptyAudioException implements Exception {} - -class RecordingDialog extends StatefulWidget { - const RecordingDialog({ - super.key, - }); - - @override - RecordingDialogState createState() => RecordingDialogState(); -} - -class RecordingDialogState extends State { - Timer? _recorderSubscription; - Duration _duration = Duration.zero; - - // #Pangea - // bool error = false; - Object? error; - bool _loading = true; - // Pangea# - - final _audioRecorder = AudioRecorder(); - final List amplitudeTimeline = []; - - String? fileName; - - Future startRecording() async { - final store = Matrix.of(context).store; - try { - // #Pangea - // final codec = kIsWeb - // // Web seems to create webm instead of ogg when using opus encoder - // // which does not play on iOS right now. So we use wav for now: - // ? AudioEncoder.wav - // // Everywhere else we use opus if supported by the platform: - // : await _audioRecorder.isEncoderSupported(AudioEncoder.opus) - // ? AudioEncoder.opus - // : AudioEncoder.aacLc; - const codec = AudioEncoder.wav; - // Pangea# - fileName = - 'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}'; - String? path; - if (!kIsWeb) { - final tempDir = await getTemporaryDirectory(); - path = path_lib.join(tempDir.path, fileName); - } - - final result = await _audioRecorder.hasPermission(); - if (result != true) { - // #Pangea - throw PermissionException(); - // setState(() => error = true); - // return; - // Pangea# - } - await WakelockPlus.enable(); - - await _audioRecorder.start( - RecordConfig( - bitRate: AppSettings.audioRecordingBitRate.getItem(store), - sampleRate: AppSettings.audioRecordingSamplingRate.getItem(store), - numChannels: AppSettings.audioRecordingNumChannels.getItem(store), - autoGain: AppSettings.audioRecordingAutoGain.getItem(store), - echoCancel: AppSettings.audioRecordingEchoCancel.getItem(store), - noiseSuppress: AppSettings.audioRecordingNoiseSuppress.getItem(store), - encoder: codec, - ), - path: path ?? '', - ); - - // #Pangea - // setState(() => _duration = Duration.zero); - setState(() { - _duration = Duration.zero; - _loading = false; - }); - // Pangea# - _recorderSubscription?.cancel(); - _recorderSubscription = - Timer.periodic(const Duration(milliseconds: 100), (_) async { - final amplitude = await _audioRecorder.getAmplitude(); - var value = 100 + amplitude.current * 2; - value = value < 1 ? 1 : value; - amplitudeTimeline.add(value); - setState(() { - _duration += const Duration(milliseconds: 100); - }); - }); - // #Pangea - // } catch (_) { - // setState(() => error = true); - } catch (e) { - setState(() => error = e); - // Pangea# - rethrow; - } - } - - @override - void initState() { - super.initState(); - startRecording(); - } - - @override - void dispose() { - WakelockPlus.disable(); - _recorderSubscription?.cancel(); - _audioRecorder.stop(); - super.dispose(); - } - - void _stopAndSend() async { - _recorderSubscription?.cancel(); - final path = await _audioRecorder.stop(); - - if (path == null) throw ('Recording failed!'); - const waveCount = AudioPlayerWidget.wavesCount; - final step = amplitudeTimeline.length < waveCount - ? 1 - : (amplitudeTimeline.length / waveCount).round(); - final waveform = []; - for (var i = 0; i < amplitudeTimeline.length; i += step) { - waveform.add((amplitudeTimeline[i] / 100 * 1024).round()); - } - - // #Pangea - if (amplitudeTimeline.isEmpty || amplitudeTimeline.every((e) => e <= 1)) { - if (mounted) { - setState(() => error = EmptyAudioException()); - } - return; - } - // Pangea# - - Navigator.of(context, rootNavigator: false).pop( - RecordingResult( - path: path, - duration: _duration.inMilliseconds, - waveform: waveform, - fileName: fileName, - ), - ); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - const maxDecibalWidth = 64.0; - final time = - '${_duration.inMinutes.toString().padLeft(2, '0')}:${(_duration.inSeconds % 60).toString().padLeft(2, '0')}'; - // #Pangea - // final content = error - // ? Text(L10n.of(context).oopsSomethingWentWrong) - final content = error != null - ? ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 250.0), - child: error is PermissionException - ? Text(L10n.of(context).recordingPermissionDenied) - : kIsWeb && error is! EmptyAudioException - ? Text(L10n.of(context).genericWebRecordingError) - : Text(error!.toLocalizedString(context)), - ) - // Pangea# - : Row( - children: [ - Container( - width: 16, - height: 16, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(32), - color: Colors.red, - ), - ), - Expanded( - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: amplitudeTimeline.reversed - .take(26) - .toList() - .reversed - .map( - (amplitude) => Container( - margin: const EdgeInsets.only(left: 2), - width: 4, - decoration: BoxDecoration( - color: theme.colorScheme.primary, - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - ), - height: maxDecibalWidth * (amplitude / 100), - ), - ) - .toList(), - ), - ), - const SizedBox(width: 8), - SizedBox( - width: 48, - // #Pangea - // child: Text(time), - child: _loading - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive(), - ) - : Text(time), - // Pangea# - ), - ], - ); - if (PlatformInfos.isCupertinoStyle) { - return CupertinoAlertDialog( - content: content, - actions: [ - CupertinoDialogAction( - onPressed: () => Navigator.of(context, rootNavigator: false).pop(), - child: Text( - L10n.of(context).cancel, - style: TextStyle( - color: theme.textTheme.bodyMedium?.color?.withAlpha(150), - ), - ), - ), - // #Pangea - // if (error != true) - if (error == null) - // Pangea# - CupertinoDialogAction( - onPressed: _stopAndSend, - child: Text(L10n.of(context).send), - ), - ], - ); - } - return AlertDialog( - content: content, - actions: [ - TextButton( - onPressed: () => Navigator.of(context, rootNavigator: false).pop(), - child: Text( - L10n.of(context).cancel, - style: TextStyle( - color: theme.colorScheme.error, - ), - ), - ), - // #Pangea - // if (error != true) - if (error == null) - // Pangea# - TextButton( - onPressed: _stopAndSend, - child: Text(L10n.of(context).send), - ), - ], - ); - } -} - -class RecordingResult { - final String path; - final int duration; - final List waveform; - final String? fileName; - - const RecordingResult({ - required this.path, - required this.duration, - required this.waveform, - required this.fileName, - }); -} - -extension on AudioEncoder { - String get fileExtension { - switch (this) { - case AudioEncoder.aacLc: - case AudioEncoder.aacEld: - case AudioEncoder.aacHe: - return 'm4a'; - case AudioEncoder.opus: - return 'ogg'; - case AudioEncoder.wav: - return 'wav'; - case AudioEncoder.amrNb: - case AudioEncoder.amrWb: - case AudioEncoder.flac: - case AudioEncoder.pcm16bits: - throw UnsupportedError('Not yet used'); - } - } -} diff --git a/lib/pages/chat/recording_input_row.dart b/lib/pages/chat/recording_input_row.dart new file mode 100644 index 000000000..2432db36d --- /dev/null +++ b/lib/pages/chat/recording_input_row.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pages/chat/recording_view_model.dart'; + +class RecordingInputRow extends StatelessWidget { + final RecordingViewModelState state; + final Future Function(String, int, List, String?) onSend; + const RecordingInputRow({ + required this.state, + required this.onSend, + super.key, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + const maxDecibalWidth = 36.0; + final time = + '${state.duration.inMinutes.toString().padLeft(2, '0')}:${(state.duration.inSeconds % 60).toString().padLeft(2, '0')}'; + return Row( + children: [ + IconButton( + tooltip: L10n.of(context).cancel, + icon: const Icon(Icons.delete_outlined), + color: theme.colorScheme.error, + onPressed: state.cancel, + ), + if (state.isPaused) + IconButton( + tooltip: L10n.of(context).resume, + icon: const Icon(Icons.play_circle_outline_outlined), + onPressed: state.resume, + ) + else + IconButton( + tooltip: L10n.of(context).pause, + icon: const Icon(Icons.pause_circle_outline_outlined), + onPressed: state.pause, + ), + Text(time), + const SizedBox(width: 8), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + const width = 4; + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: state.amplitudeTimeline.reversed + .take((constraints.maxWidth / (width + 2)).floor()) + .toList() + .reversed + .map( + (amplitude) => Container( + margin: const EdgeInsets.only(left: 2), + width: width.toDouble(), + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(2), + ), + height: maxDecibalWidth * (amplitude / 100), + ), + ) + .toList(), + ); + }, + ), + ), + IconButton( + style: IconButton.styleFrom( + disabledBackgroundColor: theme.bubbleColor.withAlpha(128), + backgroundColor: theme.bubbleColor, + foregroundColor: theme.onBubbleColor, + ), + tooltip: L10n.of(context).sendAudio, + icon: state.isSending + ? const SizedBox.square( + dimension: 24, + child: CircularProgressIndicator.adaptive(), + ) + : const Icon(Icons.send_outlined), + onPressed: state.isSending ? null : () => state.stopAndSend(onSend), + ), + ], + ); + } +} diff --git a/lib/pages/chat/recording_view_model.dart b/lib/pages/chat/recording_view_model.dart new file mode 100644 index 000000000..c65e81246 --- /dev/null +++ b/lib/pages/chat/recording_view_model.dart @@ -0,0 +1,263 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:matrix/matrix.dart'; +import 'package:path/path.dart' as path_lib; +import 'package:path_provider/path_provider.dart'; +import 'package:record/record.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +import 'package:fluffychat/config/setting_keys.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; +import 'package:fluffychat/widgets/matrix.dart'; +import 'events/audio_player.dart'; + +// #Pangea +class PermissionException implements Exception {} + +class EmptyAudioException implements Exception {} +// Pangea# + +class RecordingViewModel extends StatefulWidget { + final Widget Function(BuildContext, RecordingViewModelState) builder; + + const RecordingViewModel({ + required this.builder, + super.key, + }); + + @override + RecordingViewModelState createState() => RecordingViewModelState(); +} + +class RecordingViewModelState extends State { + Timer? _recorderSubscription; + Duration duration = Duration.zero; + + // #Pangea + // bool error = false; + Object? error; + bool loading = true; + // Pangea# + bool isSending = false; + + bool get isRecording => _audioRecorder != null; + + AudioRecorder? _audioRecorder; + final List amplitudeTimeline = []; + + String? fileName; + + bool isPaused = false; + + Future startRecording(Room room) async { + room.client.getConfig(); // Preload server file configuration. + if (PlatformInfos.isAndroid) { + final info = await DeviceInfoPlugin().androidInfo; + if (info.version.sdkInt < 19) { + showOkAlertDialog( + context: context, + title: L10n.of(context).unsupportedAndroidVersion, + message: L10n.of(context).unsupportedAndroidVersionLong, + okLabel: L10n.of(context).close, + ); + return; + } + } + if (await AudioRecorder().hasPermission() == false) return; + + final store = Matrix.of(context).store; + + final audioRecorder = _audioRecorder ??= AudioRecorder(); + setState(() {}); + + try { + // #Pangea + // final codec = kIsWeb + // // Web seems to create webm instead of ogg when using opus encoder + // // which does not play on iOS right now. So we use wav for now: + // ? AudioEncoder.wav + // // Everywhere else we use opus if supported by the platform: + // : await audioRecorder.isEncoderSupported(AudioEncoder.opus) + // ? AudioEncoder.opus + // : AudioEncoder.aacLc; + const codec = AudioEncoder.wav; + // Pangea# + fileName = + 'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}'; + String? path; + if (!kIsWeb) { + final tempDir = await getTemporaryDirectory(); + path = path_lib.join(tempDir.path, fileName); + } + + final result = await audioRecorder.hasPermission(); + if (result != true) { + // #Pangea + throw PermissionException(); + // setState(() => error = true); + // return; + // Pangea# + } + await WakelockPlus.enable(); + + await audioRecorder.start( + RecordConfig( + bitRate: AppSettings.audioRecordingBitRate.getItem(store), + sampleRate: AppSettings.audioRecordingSamplingRate.getItem(store), + numChannels: AppSettings.audioRecordingNumChannels.getItem(store), + autoGain: AppSettings.audioRecordingAutoGain.getItem(store), + echoCancel: AppSettings.audioRecordingEchoCancel.getItem(store), + noiseSuppress: AppSettings.audioRecordingNoiseSuppress.getItem(store), + encoder: codec, + ), + path: path ?? '', + ); + // #Pangea + // setState(() => duration = Duration.zero); + setState(() { + duration = Duration.zero; + loading = false; + }); + // Pangea# + _subscribe(); + // #Pangea + // } catch (_) { + // setState(() => error = true); + } catch (e) { + setState(() => error = e); + // Pangea# + rethrow; + } + } + + @override + void dispose() { + _reset(); + super.dispose(); + } + + void _subscribe() { + _recorderSubscription?.cancel(); + _recorderSubscription = + Timer.periodic(const Duration(milliseconds: 100), (_) async { + final amplitude = await _audioRecorder!.getAmplitude(); + var value = 100 + amplitude.current * 2; + value = value < 1 ? 1 : value; + amplitudeTimeline.add(value); + setState(() { + duration += const Duration(milliseconds: 100); + }); + }); + } + + void _reset() { + WakelockPlus.disable(); + _recorderSubscription?.cancel(); + _audioRecorder?.stop(); + _audioRecorder = null; + isSending = false; + error = false; + fileName = null; + duration = Duration.zero; + amplitudeTimeline.clear(); + isPaused = false; + } + + void cancel() { + setState(() { + _reset(); + }); + } + + void pause() { + _audioRecorder?.pause(); + _recorderSubscription?.cancel(); + setState(() { + isPaused = true; + }); + } + + void resume() { + _audioRecorder?.resume(); + _subscribe(); + setState(() { + isPaused = false; + }); + } + + void stopAndSend( + Future Function( + String path, + int duration, + List waveform, + String? fileName, + ) onSend, + ) async { + _recorderSubscription?.cancel(); + final path = await _audioRecorder?.stop(); + + if (path == null) throw ('Recording failed!'); + const waveCount = AudioPlayerWidget.wavesCount; + final step = amplitudeTimeline.length < waveCount + ? 1 + : (amplitudeTimeline.length / waveCount).round(); + final waveform = []; + for (var i = 0; i < amplitudeTimeline.length; i += step) { + waveform.add((amplitudeTimeline[i] / 100 * 1024).round()); + } + + // #Pangea + if (amplitudeTimeline.isEmpty || amplitudeTimeline.every((e) => e <= 1)) { + if (mounted) { + setState(() => error = EmptyAudioException()); + } + return; + } + // Pangea# + + setState(() { + isSending = true; + }); + try { + await onSend(path, duration.inMilliseconds, waveform, fileName); + } catch (e, s) { + Logs().e('Unable to send voice message', e, s); + setState(() { + isSending = false; + }); + return; + } + + cancel(); + } + + @override + Widget build(BuildContext context) => widget.builder(context, this); +} + +extension on AudioEncoder { + String get fileExtension { + switch (this) { + case AudioEncoder.aacLc: + case AudioEncoder.aacEld: + case AudioEncoder.aacHe: + return 'm4a'; + case AudioEncoder.opus: + return 'ogg'; + case AudioEncoder.wav: + return 'wav'; + case AudioEncoder.amrNb: + case AudioEncoder.amrWb: + case AudioEncoder.flac: + case AudioEncoder.pcm16bits: + throw UnsupportedError('Not yet used'); + } + } +} diff --git a/lib/pangea/chat/widgets/pangea_chat_input_row.dart b/lib/pangea/chat/widgets/pangea_chat_input_row.dart index fdff8a030..4606d39bd 100644 --- a/lib/pangea/chat/widgets/pangea_chat_input_row.dart +++ b/lib/pangea/chat/widgets/pangea_chat_input_row.dart @@ -7,6 +7,8 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/input_bar.dart'; +import 'package:fluffychat/pages/chat/recording_input_row.dart'; +import 'package:fluffychat/pages/chat/recording_view_model.dart'; import 'package:fluffychat/pangea/bot/utils/bot_room_extension.dart'; import 'package:fluffychat/pangea/choreographer/choreo_constants.dart'; import 'package:fluffychat/pangea/choreographer/choreographer_send_button.dart'; @@ -47,211 +49,233 @@ class PangeaChatInputRow extends StatelessWidget { Radius.circular(8.0), ), ), - child: Row( - key: MatrixState.pAnyState - .layerLinkAndKey(ChoreoConstants.inputTransformTargetKey) - .key, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(width: 4), - ValueListenableBuilder( - valueListenable: controller.sendController, - builder: (context, text, __) { - final isBotDM = controller.room.isBotDM; - return AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - height: height, - width: text.text.isEmpty && - !controller.choreographer.itController.open.value - ? height - : 0, - alignment: Alignment.center, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - child: PopupMenuButton( - useRootNavigator: true, - icon: const Icon(Icons.add_outlined), - onSelected: controller.onAddPopupMenuButtonSelected, - itemBuilder: (BuildContext context) => - >[ - if (!isBotDM) - PopupMenuItem( - value: 'file', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - child: Icon(Icons.attachment_outlined), + child: RecordingViewModel( + builder: (context, recordingViewModel) { + if (recordingViewModel.isRecording) { + return RecordingInputRow( + state: recordingViewModel, + onSend: controller.onVoiceMessageSend, + ); + } + return Row( + key: MatrixState.pAnyState + .layerLinkAndKey(ChoreoConstants.inputTransformTargetKey) + .key, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(width: 4), + ValueListenableBuilder( + valueListenable: controller.sendController, + builder: (context, text, __) { + final isBotDM = controller.room.isBotDM; + return AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + height: height, + width: text.text.isEmpty && + !controller + .choreographer.itController.open.value + ? height + : 0, + alignment: Alignment.center, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: PopupMenuButton( + useRootNavigator: true, + icon: const Icon(Icons.add_outlined), + onSelected: controller.onAddPopupMenuButtonSelected, + itemBuilder: (BuildContext context) => + >[ + if (!isBotDM) + PopupMenuItem( + value: 'file', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + child: Icon(Icons.attachment_outlined), + ), + title: Text(L10n.of(context).sendFile), + contentPadding: const EdgeInsets.all(0), + ), + ), + PopupMenuItem( + value: 'image', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + child: Icon(Icons.image_outlined), + ), + title: Text(L10n.of(context).sendImage), + contentPadding: const EdgeInsets.all(0), ), - title: Text(L10n.of(context).sendFile), - contentPadding: const EdgeInsets.all(0), ), - ), - PopupMenuItem( - value: 'image', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - child: Icon(Icons.image_outlined), - ), - title: Text(L10n.of(context).sendImage), - contentPadding: const EdgeInsets.all(0), + if (PlatformInfos.isMobile) + PopupMenuItem( + value: 'camera', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.purple, + foregroundColor: Colors.white, + child: Icon(Icons.camera_alt_outlined), + ), + title: Text(L10n.of(context).openCamera), + contentPadding: const EdgeInsets.all(0), + ), + ), + if (!isBotDM) + if (PlatformInfos.isMobile) + PopupMenuItem( + value: 'camera-video', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + child: Icon(Icons.videocam_outlined), + ), + title: Text( + L10n.of(context).openVideoCamera, + ), + contentPadding: const EdgeInsets.all(0), + ), + ), + if (!isBotDM) + if (PlatformInfos.isMobile) + PopupMenuItem( + value: 'location', + child: ListTile( + leading: const CircleAvatar( + backgroundColor: Colors.brown, + foregroundColor: Colors.white, + child: Icon(Icons.gps_fixed_outlined), + ), + title: + Text(L10n.of(context).shareLocation), + contentPadding: const EdgeInsets.all(0), + ), + ), + ], + ), + ); + }, + ), + if (FluffyThemes.isColumnMode(context)) + Container( + height: height, + width: height, + alignment: Alignment.center, + child: IconButton( + tooltip: L10n.of(context).emojis, + icon: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.scaled, + fillColor: Colors.transparent, + child: child, + ); + }, + child: Icon( + controller.showEmojiPicker + ? Icons.keyboard + : Icons.add_reaction_outlined, + key: ValueKey(controller.showEmojiPicker), ), ), - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'camera', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.purple, - foregroundColor: Colors.white, - child: Icon(Icons.camera_alt_outlined), - ), - title: Text(L10n.of(context).openCamera), - contentPadding: const EdgeInsets.all(0), - ), + onPressed: controller.emojiPickerAction, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 0.0), + child: InputBar( + room: controller.room, + minLines: 1, + maxLines: 8, + autofocus: !PlatformInfos.isMobile, + keyboardType: TextInputType.multiline, + textInputAction: AppConfig.sendOnEnter == true && + PlatformInfos.isMobile + ? TextInputAction.send + : null, + onSubmitted: (_) => controller.onInputBarSubmitted(), + onSubmitImage: controller.sendImageFromClipBoard, + focusNode: controller.inputFocus, + controller: controller.sendController, + decoration: const InputDecoration( + contentPadding: EdgeInsets.only( + left: 6.0, + right: 6.0, + bottom: 6.0, + top: 3.0, ), - if (!isBotDM) - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'camera-video', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - child: Icon(Icons.videocam_outlined), - ), - title: Text(L10n.of(context).openVideoCamera), - contentPadding: const EdgeInsets.all(0), - ), - ), - if (!isBotDM) - if (PlatformInfos.isMobile) - PopupMenuItem( - value: 'location', - child: ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.brown, - foregroundColor: Colors.white, - child: Icon(Icons.gps_fixed_outlined), - ), - title: Text(L10n.of(context).shareLocation), - contentPadding: const EdgeInsets.all(0), - ), - ), - ], - ), - ); - }, - ), - if (FluffyThemes.isColumnMode(context)) - Container( - height: height, - width: height, - alignment: Alignment.center, - child: IconButton( - tooltip: L10n.of(context).emojis, - icon: PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.scaled, - fillColor: Colors.transparent, - child: child, - ); - }, - child: Icon( - controller.showEmojiPicker - ? Icons.keyboard - : Icons.add_reaction_outlined, - key: ValueKey(controller.showEmojiPicker), + disabledBorder: InputBorder.none, + hintMaxLines: 1, + border: InputBorder.none, + enabledBorder: InputBorder.none, + filled: false, + ), + onChanged: controller.onInputBarChanged, + choreographer: controller.choreographer, + showNextMatch: controller.showNextMatch, ), ), - onPressed: controller.emojiPickerAction, ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 0.0), - child: InputBar( - room: controller.room, - minLines: 1, - maxLines: 8, - autofocus: !PlatformInfos.isMobile, - keyboardType: TextInputType.multiline, - textInputAction: AppConfig.sendOnEnter == true && - PlatformInfos.isMobile - ? TextInputAction.send - : null, - onSubmitted: (_) => controller.onInputBarSubmitted(), - onSubmitImage: controller.sendImageFromClipBoard, - focusNode: controller.inputFocus, - controller: controller.sendController, - decoration: const InputDecoration( - contentPadding: EdgeInsets.only( - left: 6.0, - right: 6.0, - bottom: 6.0, - top: 3.0, - ), - disabledBorder: InputBorder.none, - hintMaxLines: 1, - border: InputBorder.none, - enabledBorder: InputBorder.none, - filled: false, - ), - onChanged: controller.onInputBarChanged, + StartIGCButton( + key: ValueKey(controller.choreographer), + onPressed: () => + controller.onRequestWritingAssistance(manual: true), choreographer: controller.choreographer, - showNextMatch: controller.showNextMatch, + initialState: state, + initialForegroundColor: state.stateColor(context), + initialBackgroundColor: state.backgroundColor(context), ), - ), - ), - StartIGCButton( - key: ValueKey(controller.choreographer), - onPressed: () => - controller.onRequestWritingAssistance(manual: true), - choreographer: controller.choreographer, - initialState: state, - initialForegroundColor: state.stateColor(context), - initialBackgroundColor: state.backgroundColor(context), - ), - ValueListenableBuilder( - valueListenable: controller.sendController, - builder: (context, text, __) { - return Container( - height: height, - width: height, - alignment: Alignment.center, - child: PlatformInfos.platformCanRecord && - text.text.isEmpty && - !controller.choreographer.itController.open.value - ? FloatingActionButton.small( - tooltip: L10n.of(context).voiceMessage, - onPressed: controller.voiceMessageAction, - elevation: 0, - heroTag: null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(height), - ), - backgroundColor: theme.bubbleColor, - foregroundColor: theme.onBubbleColor, - child: const Icon(Icons.mic_none_outlined), - ) - : ChoreographerSendButton( - controller: controller, - ), - ); - }, - ), - ], + ValueListenableBuilder( + valueListenable: controller.sendController, + builder: (context, text, __) { + return Container( + height: height, + width: height, + alignment: Alignment.center, + child: PlatformInfos.platformCanRecord && + text.text.isEmpty && + !controller + .choreographer.itController.open.value + ? IconButton( + tooltip: L10n.of(context).voiceMessage, + onPressed: () => ScaffoldMessenger.of(context) + .showSnackBar( + SnackBar( + content: Text( + L10n.of(context) + .longPressToRecordVoiceMessage, + ), + ), + ), + onLongPress: () => recordingViewModel + .startRecording(controller.room), + style: IconButton.styleFrom( + backgroundColor: theme.bubbleColor, + foregroundColor: theme.onBubbleColor, + ), + icon: const Icon(Icons.mic_none_outlined), + ) + : ChoreographerSendButton( + controller: controller, + ), + ); + }, + ), + ], + ); + }, ), ), ),