From 627e6ee2dfb339c037e28bfe6c2faef201895421 Mon Sep 17 00:00:00 2001 From: MohammadTaha Basiri Date: Wed, 9 Feb 2022 12:00:47 +0330 Subject: [PATCH] D1APP-53 direct (waiting for waveform descussoin) --- lib/models/message_data/message_data.dart | 18 ++ .../home/direct/{widgets => }/direct.dart | 0 lib/pages/home/direct/direct_state.dart | 58 +++++- lib/pages/home/direct/widgets/message.dart | 182 ++++++++++-------- .../home/direct/widgets/message_box.dart | 76 ++++++-- lib/pages/home/widgets/audio_visualizer.dart | 88 +++++---- lib/routes/route_generator.dart | 2 +- lib/services/network/request_helper.dart | 5 +- lib/utils/date_time.dart | 25 +++ 9 files changed, 303 insertions(+), 151 deletions(-) rename lib/pages/home/direct/{widgets => }/direct.dart (100%) diff --git a/lib/models/message_data/message_data.dart b/lib/models/message_data/message_data.dart index 8cb4b45..00f20b9 100644 --- a/lib/models/message_data/message_data.dart +++ b/lib/models/message_data/message_data.dart @@ -1,3 +1,7 @@ +import 'dart:io'; + +import 'package:just_waveform/just_waveform.dart'; + import 'radar_attachment.dart'; class MessageData { @@ -8,6 +12,8 @@ class MessageData { final bool readed; final String createdAt; final RadarAttachment? radar; + final File? audioFile; + final Waveform? waveform; const MessageData({ required this.id, @@ -17,6 +23,8 @@ class MessageData { required this.text, required this.audio, required this.radar, + required this.waveform, + this.audioFile, }); factory MessageData.fromJson(Map json) => MessageData( @@ -26,6 +34,16 @@ class MessageData { writedByAdmin: json['writedByAdmin'], readed: json['readed'], createdAt: json['createdAt'], + waveform: json['waveForm'] != null + ? Waveform( + version: json['version'], + flags: json['flags'], + sampleRate: json['sampleRate'], + samplesPerPixel: json['samplesPerPixel'], + length: json['length'], + data: json['data'], + ) + : null, radar: json['radar'] == null ? null : RadarAttachment.fromJson(json['radar'] as Map), diff --git a/lib/pages/home/direct/widgets/direct.dart b/lib/pages/home/direct/direct.dart similarity index 100% rename from lib/pages/home/direct/widgets/direct.dart rename to lib/pages/home/direct/direct.dart diff --git a/lib/pages/home/direct/direct_state.dart b/lib/pages/home/direct/direct_state.dart index 678b4e1..2365545 100644 --- a/lib/pages/home/direct/direct_state.dart +++ b/lib/pages/home/direct/direct_state.dart @@ -2,11 +2,13 @@ import 'dart:io'; import 'package:didvan/models/enums.dart'; import 'package:didvan/models/message_data/message_data.dart'; +import 'package:didvan/models/message_data/radar_attachment.dart'; import 'package:didvan/providers/core_provider.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request_helper.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; +import 'package:just_waveform/just_waveform.dart'; import 'package:record/record.dart'; class DirectState extends CoreProvier { @@ -15,7 +17,10 @@ class DirectState extends CoreProvier { late final int typeId; final Map> dailyMessages = {}; + String? text; + RadarAttachment? replyRadar; File? recordedFile; + Waveform? waveform; bool isRecording = false; @@ -27,14 +32,7 @@ class DirectState extends CoreProvier { final messageDatas = service.result['messages']; for (var i = 0; i < messageDatas.length; i++) { messages.add(MessageData.fromJson(messageDatas[i])); - final createdAt = messages.last.createdAt.split('T').first; - if (!dailyMessages.containsKey(createdAt)) { - dailyMessages.addAll({ - createdAt: [messages.last.id] - }); - } else { - dailyMessages[createdAt]!.add(messages.last.id); - } + _addToDailyGrouped(); } appState = AppState.idle; return; @@ -58,7 +56,7 @@ class DirectState extends CoreProvier { notifyListeners(); } - Future stopRecording(bool sendImidiately) async { + Future stopRecording({required bool sendImidiately}) async { final path = await _recorder.stop(); isRecording = false; if (path == null) { @@ -78,5 +76,45 @@ class DirectState extends CoreProvier { } } - Future sendMessage() async {} + void _addToDailyGrouped() { + final createdAt = messages.last.createdAt.split('T').first; + if (!dailyMessages.containsKey(createdAt)) { + dailyMessages.addAll({ + createdAt: [messages.last.id] + }); + } else { + dailyMessages[createdAt]!.add(messages.last.id); + } + } + + Future sendMessage() async { + if (text == null || text!.isEmpty && recordedFile == null) return; + final body = {}; + if (text != null) { + body.addAll({'text': text}); + messages.add( + MessageData( + id: 0, + writedByAdmin: false, + readed: false, + createdAt: DateTime.now().toString(), + text: text, + audio: null, + audioFile: recordedFile, + radar: replyRadar, + waveform: waveform, + ), + ); + } + _addToDailyGrouped(); + if (replyRadar != null) { + body.addAll({'radarId': replyRadar!.id}); + } + text = null; + recordedFile = null; + notifyListeners(); + final service = + RequestService(RequestHelper.sendDirectMessage(typeId), body: body); + await service.post(); + } } diff --git a/lib/pages/home/direct/widgets/message.dart b/lib/pages/home/direct/widgets/message.dart index f37d93c..1f4c1d5 100644 --- a/lib/pages/home/direct/widgets/message.dart +++ b/lib/pages/home/direct/widgets/message.dart @@ -2,7 +2,9 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/models/message_data/message_data.dart'; import 'package:didvan/pages/home/direct/direct_state.dart'; +import 'package:didvan/pages/home/widgets/audio_visualizer.dart'; import 'package:didvan/utils/date_time.dart'; +import 'package:didvan/widgets/didvan/divider.dart'; import 'package:didvan/widgets/didvan/text.dart'; import 'package:didvan/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; @@ -15,19 +17,18 @@ class Message extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only( - right: message.writedByAdmin ? 20 : 0, - left: !message.writedByAdmin ? 20 : 0, - ), - child: Column( - children: [ - if (message.id == - context - .read() - .dailyMessages[message.createdAt.split('T').first]! - .last) - Container( + final firstMessageOfGroupId = context + .read() + .dailyMessages[message.createdAt.split('T').first]! + .last; + return Column( + crossAxisAlignment: message.writedByAdmin + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + if (message.id == firstMessageOfGroupId) + Center( + child: Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(4), decoration: BoxDecoration( @@ -42,23 +43,45 @@ class Message extends StatelessWidget { : Theme.of(context).colorScheme.black, ), ), - _MessageContainer( - isAttachment: false, - writedByAdmin: message.writedByAdmin, - child: - message.text != null ? DidvanText(message.text!) : Container(), ), - if (message.radar != null) - DidvanText( - 'لینک به مطلب زیر:', - style: Theme.of(context).textTheme.overline, - color: Theme.of(context).colorScheme.primary, - ), - if (message.radar != null) const SizedBox(height: 4), - if (message.radar != null) _ReplyRadarOverview(message: message), - if (message.radar != null) const SizedBox(height: 4), - ], - ), + Padding( + padding: EdgeInsets.only( + right: message.writedByAdmin ? 20 : 0, + left: !message.writedByAdmin ? 20 : 0, + ), + child: Column( + crossAxisAlignment: message.writedByAdmin + ? CrossAxisAlignment.start + : CrossAxisAlignment.end, + children: [ + _MessageContainer( + writedByAdmin: message.writedByAdmin, + child: Column( + children: [ + if (message.text != null) DidvanText(message.text!), + if (message.audio != null) + AudioVisualizer( + audioUrl: message.audio, + waveform: message.waveform, + ), + if (message.radar != null) const DidvanDivider(), + if (message.radar != null) const SizedBox(height: 4), + if (message.radar != null) + _ReplyRadarOverview(message: message), + if (message.radar != null) const SizedBox(height: 4), + ], + ), + ), + const SizedBox(height: 4), + DidvanText( + DateTimeUtils.timeWithAmPm(message.createdAt), + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.caption, + ), + ], + ), + ), + ], ); } } @@ -70,70 +93,63 @@ class _ReplyRadarOverview extends StatelessWidget { @override Widget build(BuildContext context) { - return _MessageContainer( - writedByAdmin: message.writedByAdmin, - isAttachment: true, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SkeletonImage( - imageUrl: message.radar!.image, - height: 80, - width: 80, - ), - const SizedBox(width: 8), - Expanded( - child: SizedBox( - height: 80, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - DidvanText( - message.radar!.title, - style: Theme.of(context).textTheme.bodyText1, - maxLines: 2, - overflow: TextOverflow.ellipsis, - color: Theme.of(context).colorScheme.focusedBorder, - ), - Row( - children: [ - DidvanText( - 'رادار ' + message.radar!.categories.first.label, - style: Theme.of(context).textTheme.overline, - color: Theme.of(context).colorScheme.focusedBorder, - ), - const Spacer(), - DidvanText( - DateTimeUtils.momentGenerator( - message.radar!.createdAt) + - ' | خواندن در ' + - message.radar!.timeToRead.toString() + - ' دقیقه', - color: Theme.of(context).colorScheme.focusedBorder, - style: Theme.of(context).textTheme.overline, - ), - // DidvanText('text'), - ], - ), - ], - ), + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SkeletonImage( + imageUrl: message.radar!.image, + height: 52, + width: 52, + ), + const SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 52, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DidvanText( + message.radar!.title, + style: Theme.of(context).textTheme.bodyText1, + maxLines: 1, + overflow: TextOverflow.ellipsis, + color: Theme.of(context).colorScheme.focusedBorder, + ), + Row( + children: [ + DidvanText( + 'رادار ' + message.radar!.categories.first.label, + style: Theme.of(context).textTheme.overline, + color: Theme.of(context).colorScheme.focusedBorder, + ), + const Spacer(), + DidvanText( + DateTimeUtils.momentGenerator(message.radar!.createdAt) + + ' | خواندن در ' + + message.radar!.timeToRead.toString() + + ' دقیقه', + color: Theme.of(context).colorScheme.focusedBorder, + style: Theme.of(context).textTheme.overline, + ), + // DidvanText('text'), + ], + ), + ], ), ), - ], - ), + ), + ], ); } } class _MessageContainer extends StatelessWidget { final bool writedByAdmin; - final bool isAttachment; final Widget child; const _MessageContainer({ Key? key, required this.writedByAdmin, - required this.isAttachment, required this.child, }) : super(key: key); @@ -143,8 +159,8 @@ class _MessageContainer extends StatelessWidget { padding: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: DesignConfig.highBorderRadius.copyWith( - bottomLeft: writedByAdmin && !isAttachment ? Radius.zero : null, - bottomRight: !writedByAdmin && !isAttachment ? Radius.zero : null, + bottomLeft: writedByAdmin ? Radius.zero : null, + bottomRight: !writedByAdmin ? Radius.zero : null, ), color: writedByAdmin ? null : Theme.of(context).colorScheme.focused, border: Border.all( diff --git a/lib/pages/home/direct/widgets/message_box.dart b/lib/pages/home/direct/widgets/message_box.dart index 3f2ccf4..17fb9cf 100644 --- a/lib/pages/home/direct/widgets/message_box.dart +++ b/lib/pages/home/direct/widgets/message_box.dart @@ -1,3 +1,4 @@ +import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/pages/home/direct/direct_state.dart'; @@ -36,32 +37,69 @@ class MessageBox extends StatelessWidget { } } -class _Typing extends StatelessWidget { +class _Typing extends StatefulWidget { const _Typing({Key? key}) : super(key: key); + @override + State<_Typing> createState() => _TypingState(); +} + +class _TypingState extends State<_Typing> { + final _formKey = GlobalKey(); + @override Widget build(BuildContext context) { final state = context.read(); return Row( children: [ - DidvanIconButton( - icon: DidvanIcons.mic_solid, - onPressed: state.startRecording, - size: 32, - color: Theme.of(context).colorScheme.focusedBorder, + Expanded( + flex: 2, + child: AnimatedSwitcher( + duration: DesignConfig.lowAnimationDuration, + transitionBuilder: (child, animation) => ScaleTransition( + scale: animation, + child: child, + ), + child: state.text != null && state.text!.isNotEmpty + ? DidvanIconButton( + key: const ValueKey(1), + icon: DidvanIcons.send_solid, + onPressed: () { + _formKey.currentState!.reset(); + state.sendMessage(); + }, + size: 32, + color: Theme.of(context).colorScheme.focusedBorder, + ) + : DidvanIconButton( + key: const ValueKey(2), + icon: DidvanIcons.mic_solid, + onPressed: state.startRecording, + size: 32, + color: Theme.of(context).colorScheme.focusedBorder, + ), + ), ), Expanded( - child: TextField( - textInputAction: TextInputAction.send, - decoration: InputDecoration( - border: InputBorder.none, - hintText: 'بنویسید یا پیام صوتی بگذارید...', - hintStyle: Theme.of(context) - .textTheme - .caption! - .copyWith(color: Theme.of(context).colorScheme.disabledText), + flex: 15, + child: Form( + key: _formKey, + child: TextField( + textInputAction: TextInputAction.send, + style: Theme.of(context).textTheme.bodyText2, + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'بنویسید یا پیام صوتی بگذارید...', + hintStyle: Theme.of(context).textTheme.caption!.copyWith( + color: Theme.of(context).colorScheme.disabledText), + ), + onChanged: (value) { + if (value.length <= 1) { + setState(() {}); + } + state.text = value; + }, ), - onChanged: (value) {}, ), ), ], @@ -79,7 +117,7 @@ class _Recording extends StatelessWidget { children: [ DidvanIconButton( icon: DidvanIcons.send_solid, - onPressed: () => state.stopRecording(true), + onPressed: () => state.stopRecording(sendImidiately: true), gestureSize: 52, ), Expanded( @@ -91,7 +129,7 @@ class _Recording extends StatelessWidget { DidvanIconButton( icon: DidvanIcons.stop_circle_solid, color: Theme.of(context).colorScheme.secondary, - onPressed: () => state.stopRecording(false), + onPressed: () => state.stopRecording(sendImidiately: false), size: 32, ), ], @@ -109,7 +147,7 @@ class _RecordChecking extends StatelessWidget { children: [ DidvanIconButton( icon: DidvanIcons.send_solid, - onPressed: () => state.stopRecording(true), + onPressed: () => state.stopRecording(sendImidiately: true), color: Theme.of(context).colorScheme.focusedBorder, ), Expanded( diff --git a/lib/pages/home/widgets/audio_visualizer.dart b/lib/pages/home/widgets/audio_visualizer.dart index 94fc277..f4c77dc 100644 --- a/lib/pages/home/widgets/audio_visualizer.dart +++ b/lib/pages/home/widgets/audio_visualizer.dart @@ -5,6 +5,7 @@ import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/assets.dart'; +import 'package:didvan/pages/home/direct/direct_state.dart'; import 'package:didvan/services/storage/storage.dart'; import 'package:didvan/utils/date_time.dart'; import 'package:didvan/widgets/didvan/icon_button.dart'; @@ -14,13 +15,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:just_audio/just_audio.dart'; import 'package:just_waveform/just_waveform.dart'; +import 'package:provider/provider.dart'; class AudioVisualizer extends StatefulWidget { - final File audioFile; + final File? audioFile; + final Waveform? waveform; + final String? audioUrl; const AudioVisualizer({ Key? key, - required this.audioFile, + this.audioFile, + this.waveform, + this.audioUrl, }) : super(key: key); @override @@ -34,9 +40,9 @@ class _AudioVisualizerState extends State { @override void initState() { - if (!kIsWeb) { + if (!kIsWeb && widget.audioFile != null) { waveDataStream = JustWaveform.extract( - audioInFile: widget.audioFile, + audioInFile: widget.audioFile!, waveOutFile: File(StorageService.appTempsDir + '/rec-wave.wave'), zoom: const WaveformZoom.pixelsPerSecond(100), ); @@ -86,36 +92,24 @@ class _AudioVisualizerState extends State { if (kIsWeb) { return SvgPicture.asset(Assets.record); } - return StreamBuilder( - stream: waveDataStream, - builder: (context, snapshot) { - if (snapshot.data == null) { - return const SizedBox(); - } - if (snapshot.data!.waveform == null) { - return const SizedBox(); - } - final waveform = snapshot.data!.waveform!; - return GestureDetector( - onHorizontalDragUpdate: _changePosition, - onTapDown: _changePosition, - child: SizedBox( - height: double.infinity, - width: double.infinity, - child: _AudioWaveformWidget( - waveform: waveform, - audioPlayer: _audioPlayer, - start: Duration.zero, - scale: 2, - strokeWidth: 3, - duration: waveform.duration, - waveColor: - Theme.of(context).colorScheme.focusedBorder, - ), - ), - ); - }, - ); + + if (widget.audioFile != null) { + return StreamBuilder( + stream: waveDataStream, + builder: (context, snapshot) { + if (snapshot.data == null) { + return const SizedBox(); + } + if (snapshot.data!.waveform == null) { + return const SizedBox(); + } + final waveform = snapshot.data!.waveform!; + context.read().waveform = waveform; + return _waveWidget(waveform); + }, + ); + } + return _waveWidget(widget.waveform!); }, ), ), @@ -136,6 +130,24 @@ class _AudioVisualizerState extends State { ); } + Widget _waveWidget(Waveform waveform) => GestureDetector( + onHorizontalDragUpdate: _changePosition, + onTapDown: _changePosition, + child: SizedBox( + height: double.infinity, + width: double.infinity, + child: _AudioWaveformWidget( + waveform: waveform, + audioPlayer: _audioPlayer, + start: Duration.zero, + scale: 2, + strokeWidth: 3, + duration: waveform.duration, + waveColor: Theme.of(context).colorScheme.focusedBorder, + ), + ), + ); + void _changePosition(details) { double posper = details.localPosition.dx / (MediaQuery.of(context).size.width - 200); @@ -148,12 +160,14 @@ class _AudioVisualizerState extends State { } Future _setupAudioPlayer() async { - if (kIsWeb) { + if (kIsWeb || widget.audioFile == null) { await _audioPlayer.setUrl( - widget.audioFile.uri.path.replaceAll('%3A', ':'), + kIsWeb + ? widget.audioFile!.uri.path.replaceAll('%3A', ':') + : widget.audioUrl!, ); } else { - await _audioPlayer.setFilePath(widget.audioFile.path); + await _audioPlayer.setFilePath(widget.audioFile!.path); } } diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index 2653724..2f145d4 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -2,7 +2,7 @@ import 'package:didvan/pages/authentication/authentication.dart'; import 'package:didvan/pages/authentication/authentication_state.dart'; import 'package:didvan/pages/home/comments/comments.dart'; import 'package:didvan/pages/home/comments/comments_state.dart'; -import 'package:didvan/pages/home/direct/widgets/direct.dart'; +import 'package:didvan/pages/home/direct/direct.dart'; import 'package:didvan/pages/home/direct/direct_state.dart'; import 'package:didvan/pages/home/home.dart'; import 'package:didvan/pages/home/home_state.dart'; diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index cca9c02..c48c96d 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -6,6 +6,7 @@ class RequestHelper { static const String _baseUserUrl = baseUrl + '/user'; static const String _baseRadarUrl = baseUrl + '/radar'; static const String _baseNewsUrl = baseUrl + '/news'; + static const String _baseDirectUrl = _baseUserUrl + '/direct'; static const String confirmUsername = _baseUserUrl + '/confirmUsername'; static const String login = _baseUserUrl + '/login'; @@ -18,7 +19,9 @@ class RequestHelper { _baseUserUrl + '/marked/${type ?? ''}'; static const String directTypes = baseUrl + '/direct/types'; - static String direct(int id) => _baseUserUrl + '/direct/$id'; + static String direct(int id) => _baseDirectUrl + '/$id'; + static String sendDirectMessage(int id) => + _baseDirectUrl + '/$id/sendMessage'; static String tag({ required List ids, required String type, diff --git a/lib/utils/date_time.dart b/lib/utils/date_time.dart index 4d70166..202a9ec 100644 --- a/lib/utils/date_time.dart +++ b/lib/utils/date_time.dart @@ -52,6 +52,31 @@ class DateTimeUtils { return result?.toDateTime().toString(); } + static String timeWithAmPm(String input) { + final dateTime = utcToLocalTime(input); + bool isAm = true; + int hour = 0; + int minute = 0; + if (dateTime.hour > 12) { + isAm = false; + hour = dateTime.hour - 12; + } else { + hour = dateTime.hour; + } + minute = dateTime.minute; + return '$hour:${_timeNormalizer(minute)} ${isAm ? 'ق.ظ' : 'ب.ظ'}'; + } + + static DateTime utcToLocalTime(String input) { + final dateTime = DateTime.parse(input); + return dateTime.add(const Duration(hours: 3, minutes: 30)); + } + + static String _timeNormalizer(int input) { + if (input < 10) return '0$input'; + return input.toString(); + } + static String momentGenerator(String input) { final date = DateTime.parse(input); final int seconds = (DateTime.now().difference(date).inSeconds).floor();