diff --git a/lib/api/marianumcloud/talk/chat/getChatResponse.dart b/lib/api/marianumcloud/talk/chat/getChatResponse.dart index b62b897..7055417 100644 --- a/lib/api/marianumcloud/talk/chat/getChatResponse.dart +++ b/lib/api/marianumcloud/talk/chat/getChatResponse.dart @@ -38,6 +38,7 @@ class GetChatResponseObject { Map? reactions; List? reactionsSelf; @JsonKey(fromJson: _fromJson) Map? messageParameters; + GetChatResponseObject? parent; GetChatResponseObject( this.id, @@ -53,7 +54,8 @@ class GetChatResponseObject { this.message, this.messageParameters, this.reactions, - this.reactionsSelf + this.reactionsSelf, + this.parent, ); factory GetChatResponseObject.fromJson(Map json) => _$GetChatResponseObjectFromJson(json); @@ -78,7 +80,8 @@ class GetChatResponseObject { text, null, null, - null + null, + null, ); } diff --git a/lib/api/marianumcloud/talk/chat/getChatResponse.g.dart b/lib/api/marianumcloud/talk/chat/getChatResponse.g.dart index 3da4655..0b241cb 100644 --- a/lib/api/marianumcloud/talk/chat/getChatResponse.g.dart +++ b/lib/api/marianumcloud/talk/chat/getChatResponse.g.dart @@ -52,6 +52,10 @@ GetChatResponseObject _$GetChatResponseObjectFromJson( (json['reactionsSelf'] as List?) ?.map((e) => e as String) .toList(), + json['parent'] == null + ? null + : GetChatResponseObject.fromJson( + json['parent'] as Map), ); Map _$GetChatResponseObjectToJson( @@ -74,6 +78,7 @@ Map _$GetChatResponseObjectToJson( 'reactionsSelf': instance.reactionsSelf, 'messageParameters': instance.messageParameters?.map((k, e) => MapEntry(k, e.toJson())), + 'parent': instance.parent?.toJson(), }; const _$GetRoomResponseObjectMessageActorTypeEnumMap = { diff --git a/lib/api/marianumcloud/talk/sendMessage/sendMessageParams.dart b/lib/api/marianumcloud/talk/sendMessage/sendMessageParams.dart index e9150c4..d467246 100644 --- a/lib/api/marianumcloud/talk/sendMessage/sendMessageParams.dart +++ b/lib/api/marianumcloud/talk/sendMessage/sendMessageParams.dart @@ -7,7 +7,7 @@ part 'sendMessageParams.g.dart'; @JsonSerializable(explicitToJson: true, includeIfNull: false) class SendMessageParams extends ApiParams { String message; - int? replyTo; + String? replyTo; SendMessageParams(this.message, {this.replyTo}); diff --git a/lib/api/marianumcloud/talk/sendMessage/sendMessageParams.g.dart b/lib/api/marianumcloud/talk/sendMessage/sendMessageParams.g.dart index 50255b4..76093c2 100644 --- a/lib/api/marianumcloud/talk/sendMessage/sendMessageParams.g.dart +++ b/lib/api/marianumcloud/talk/sendMessage/sendMessageParams.g.dart @@ -9,7 +9,7 @@ part of 'sendMessageParams.dart'; SendMessageParams _$SendMessageParamsFromJson(Map json) => SendMessageParams( json['message'] as String, - replyTo: json['replyTo'] as int?, + replyTo: json['replyTo'] as String?, ); Map _$SendMessageParamsToJson(SendMessageParams instance) { diff --git a/lib/app.dart b/lib/app.dart index 11ea0ac..a250380 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -11,6 +11,7 @@ import 'package:badges/badges.dart' as badges; import 'api/mhsl/breaker/getBreakers/getBreakersResponse.dart'; import 'api/mhsl/server/userIndex/update/updateUserindex.dart'; +import 'main.dart'; import 'model/breakers/Breaker.dart'; import 'model/breakers/BreakerProps.dart'; import 'model/chatList/chatListProps.dart'; @@ -28,13 +29,12 @@ import 'view/pages/timetable/timetable.dart'; class App extends StatefulWidget { const App({super.key}); - static PersistentTabController bottomNavigator = PersistentTabController(initialIndex: 0); - @override State createState() => _AppState(); } class _AppState extends State with WidgetsBindingObserver { + late Timer refetchChats; late Timer updateTimings; @@ -58,6 +58,7 @@ class _AppState extends State with WidgetsBindingObserver { @override void initState() { + Main.bottomNavigator = PersistentTabController(initialIndex: 0); WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { Provider.of(context, listen: false).run(); @@ -95,7 +96,7 @@ class _AppState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) => PersistentTabView( - controller: App.bottomNavigator, + controller: Main.bottomNavigator, navBarOverlap: const NavBarOverlap.none(), backgroundColor: Theme.of(context).colorScheme.primary, diff --git a/lib/main.dart b/lib/main.dart index 10b741b..0e1aa43 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:jiffy/jiffy.dart'; import 'package:loader_overlay/loader_overlay.dart'; +import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:provider/provider.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -74,6 +75,8 @@ Future main() async { class Main extends StatefulWidget { const Main({super.key}); + static PersistentTabController bottomNavigator = PersistentTabController(initialIndex: 0); + @override State
createState() => _MainState(); diff --git a/lib/model/chatList/chatProps.dart b/lib/model/chatList/chatProps.dart index bd0a749..80b1038 100644 --- a/lib/model/chatList/chatProps.dart +++ b/lib/model/chatList/chatProps.dart @@ -1,15 +1,23 @@ +import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; + import '../../api/apiResponse.dart'; import '../../api/marianumcloud/talk/chat/getChatCache.dart'; import '../../api/marianumcloud/talk/chat/getChatResponse.dart'; +import '../../storage/base/settingsProvider.dart'; import '../dataHolder.dart'; class ChatProps extends DataHolder { String _queryToken = ''; DateTime _lastTokenSet = DateTime.now(); + int? _referenceMessageId; GetChatResponse? _getChatResponse; GetChatResponse get getChatResponse => _getChatResponse!; + int? get getReferenceMessageId => _referenceMessageId; + set unsafeInternalSetReferenceMessageId(int? reference) => _referenceMessageId = reference; + @override List properties() => [_getChatResponse]; @@ -30,6 +38,20 @@ class ChatProps extends DataHolder { ); } + void setReferenceMessageId(int? messageId, BuildContext context, String sendToToken) { + Future.microtask(() { + _referenceMessageId = messageId; + notifyListeners(); + + var settings = Provider.of(context, listen: false); + if(messageId != null) { + settings.val(write: true).talkSettings.draftReplies[sendToToken] = messageId; + } else { + settings.val(write: true).talkSettings.draftReplies.removeWhere((key, value) => key == sendToToken); + } + }); + } + void setQueryToken(String token) { _queryToken = token; _getChatResponse = null; diff --git a/lib/notification/notificationTasks.dart b/lib/notification/notificationTasks.dart index 9719b67..b5d50cd 100644 --- a/lib/notification/notificationTasks.dart +++ b/lib/notification/notificationTasks.dart @@ -3,7 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:provider/provider.dart'; -import '../app.dart'; +import '../main.dart'; import '../model/chatList/chatListProps.dart'; import '../model/chatList/chatProps.dart'; @@ -18,6 +18,6 @@ class NotificationTasks { } static void navigateToTalk() { - App.bottomNavigator.jumpToTab(1); + Main.bottomNavigator.jumpToTab(1); } } diff --git a/lib/storage/talk/talkSettings.dart b/lib/storage/talk/talkSettings.dart index a0df6a2..7c3123a 100644 --- a/lib/storage/talk/talkSettings.dart +++ b/lib/storage/talk/talkSettings.dart @@ -7,8 +7,9 @@ class TalkSettings { bool sortFavoritesToTop; bool sortUnreadToTop; Map drafts; + Map draftReplies; - TalkSettings({required this.sortFavoritesToTop, required this.sortUnreadToTop, required this.drafts}); + TalkSettings({required this.sortFavoritesToTop, required this.sortUnreadToTop, required this.drafts, required this.draftReplies}); factory TalkSettings.fromJson(Map json) => _$TalkSettingsFromJson(json); Map toJson() => _$TalkSettingsToJson(this); diff --git a/lib/storage/talk/talkSettings.g.dart b/lib/storage/talk/talkSettings.g.dart index 83a356d..ad15998 100644 --- a/lib/storage/talk/talkSettings.g.dart +++ b/lib/storage/talk/talkSettings.g.dart @@ -10,6 +10,7 @@ TalkSettings _$TalkSettingsFromJson(Map json) => TalkSettings( sortFavoritesToTop: json['sortFavoritesToTop'] as bool, sortUnreadToTop: json['sortUnreadToTop'] as bool, drafts: Map.from(json['drafts'] as Map), + draftReplies: Map.from(json['draftReplies'] as Map), ); Map _$TalkSettingsToJson(TalkSettings instance) => @@ -17,4 +18,5 @@ Map _$TalkSettingsToJson(TalkSettings instance) => 'sortFavoritesToTop': instance.sortFavoritesToTop, 'sortUnreadToTop': instance.sortUnreadToTop, 'drafts': instance.drafts, + 'draftReplies': instance.draftReplies, }; diff --git a/lib/view/pages/talk/chatView.dart b/lib/view/pages/talk/chatView.dart index c66725f..34577bb 100644 --- a/lib/view/pages/talk/chatView.dart +++ b/lib/view/pages/talk/chatView.dart @@ -71,6 +71,7 @@ class _ChatViewState extends State { chatData: widget.room, refetch: _query, isRead: element.id <= commonRead, + selfId: widget.selfId, ) ); }); @@ -129,8 +130,8 @@ class _ChatViewState extends State { Container( color: Theme.of(context).colorScheme.background, child: TalkNavigator.isSecondaryVisible(context) - ? ChatTextfield(widget.room.token) - : SafeArea(child: ChatTextfield(widget.room.token) + ? ChatTextfield(widget.room.token, selfId: widget.selfId) + : SafeArea(child: ChatTextfield(widget.room.token, selfId: widget.selfId) ), ) ], diff --git a/lib/view/pages/talk/components/answerReference.dart b/lib/view/pages/talk/components/answerReference.dart new file mode 100644 index 0000000..ce02745 --- /dev/null +++ b/lib/view/pages/talk/components/answerReference.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart'; +import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart'; +import 'chatBubbleStyles.dart'; + +class AnswerReference extends StatelessWidget { + final BuildContext context; + final GetChatResponseObject referenceMessage; + final String? selfId; + const AnswerReference({required this.context, required this.referenceMessage, required this.selfId, super.key}); + + @override + Widget build(BuildContext context) { + var style = ChatBubbleStyles(context); + return DecoratedBox( + decoration: BoxDecoration( + color: referenceMessage.actorId == selfId + ? style.getSelfStyle(false).color!.withGreen(200).withOpacity(0.2) + : style.getRemoteStyle(false).color!.withWhite(200).withOpacity(0.2), + borderRadius: const BorderRadius.all(Radius.circular(5)), + border: Border(left: BorderSide( + color: referenceMessage.actorId == selfId + ? style.getSelfStyle(false).color!.withGreen(200) + : style.getRemoteStyle(false).color!.withWhite(200), + width: 5 + )), + ), + child: Padding( + padding: const EdgeInsets.all(5).add(const EdgeInsets.only(left: 5)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + referenceMessage.actorDisplayName, + maxLines: 1, + style: TextStyle( + overflow: TextOverflow.ellipsis, + color: referenceMessage.actorId == selfId + ? style.getSelfStyle(false).color!.withGreen(200) + : style.getRemoteStyle(false).color!.withWhite(200), + fontSize: 12, + ), + ), + Text( + RichObjectStringProcessor.parseToString(referenceMessage.message, referenceMessage.messageParameters), + maxLines: 2, + style: TextStyle( + overflow: TextOverflow.ellipsis, + color: Theme.of(context).colorScheme.onSurface, + fontSize: 12, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/pages/talk/components/chatBubble.dart b/lib/view/pages/talk/components/chatBubble.dart index 6c589c5..8110c1c 100644 --- a/lib/view/pages/talk/components/chatBubble.dart +++ b/lib/view/pages/talk/components/chatBubble.dart @@ -2,6 +2,7 @@ import 'package:better_open_file/better_open_file.dart'; import 'package:bubble/bubble.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis; import 'package:flowder/flowder.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -17,9 +18,10 @@ import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart'; import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart'; import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart'; import '../../../../model/chatList/chatProps.dart'; -import '../../../../theming/appTheme.dart'; import '../../../../widget/debug/debugTile.dart'; import '../../files/fileElement.dart'; +import 'answerReference.dart'; +import 'chatBubbleStyles.dart'; import 'chatMessage.dart'; import '../messageReactions.dart'; @@ -29,6 +31,7 @@ class ChatBubble extends StatefulWidget { final GetChatResponseObject bubbleData; final GetRoomResponseObject chatData; final bool isRead; + final String? selfId; final double spacing = 3; final double timeIconSize = 11; @@ -43,59 +46,31 @@ class ChatBubble extends StatefulWidget { required this.chatData, required this.refetch, this.isRead = false, + this.selfId, super.key}); @override State createState() => _ChatBubbleState(); } -class _ChatBubbleState extends State { - - BubbleStyle getSystemStyle() => BubbleStyle( - color: AppTheme.isDarkMode(context) ? const Color(0xff182229) : Colors.white, - borderWidth: 1, - elevation: 2, - margin: const BubbleEdges.only(bottom: 20, top: 10), - alignment: Alignment.center, - ); - - BubbleStyle getRemoteStyle(bool seamless) { - var color = AppTheme.isDarkMode(context) ? const Color(0xff202c33) : Colors.white; - return BubbleStyle( - nip: BubbleNip.leftTop, - color: seamless ? Colors.transparent : color, - borderWidth: seamless ? 0 : 1, - elevation: seamless ? 0 : 1, - margin: const BubbleEdges.only(bottom: 10, left: 10, right: 50), - alignment: Alignment.topLeft, - ); - } - - BubbleStyle getSelfStyle(bool seamless) { - var color = AppTheme.isDarkMode(context) ? const Color(0xff005c4b) : const Color(0xffd3d3d3); - return BubbleStyle( - nip: BubbleNip.rightBottom, - color: seamless ? Colors.transparent : color, - borderWidth: seamless ? 0 : 1, - elevation: seamless ? 0 : 1, - margin: const BubbleEdges.only(bottom: 10, right: 10, left: 50), - alignment: Alignment.topRight, - ); - } - +class _ChatBubbleState extends State with SingleTickerProviderStateMixin { late ChatMessage message; double downloadProgress = 0; Future? downloadCore; + late Offset _position = const Offset(0, 0); + late Offset _dragStartPosition = Offset.zero; + BubbleStyle getStyle() { + var styles = ChatBubbleStyles(context); if(widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment) { if(widget.isSender) { - return getSelfStyle(false); + return styles.getSelfStyle(false); } else { - return getRemoteStyle(false); + return styles.getRemoteStyle(false); } } else { - return getSystemStyle(); + return styles.getSystemStyle(); } } @@ -207,6 +182,17 @@ class _ChatBubbleState extends State { ], ), ), + Visibility( + visible: widget.bubbleData.isReplyable, + child: ListTile( + leading: const Icon(Icons.reply_outlined), + title: const Text('Antworten'), + onTap: () => { + Provider.of(context, listen: false).setReferenceMessageId(widget.bubbleData.id, context, widget.chatData.token), + Navigator.of(context).pop(), + }, + ), + ), Visibility( visible: canReact, child: ListTile( @@ -265,8 +251,10 @@ class _ChatBubbleState extends State { Widget build(BuildContext context) { message = ChatMessage(originalMessage: widget.bubbleData.message, originalData: widget.bubbleData.messageParameters); var showActorDisplayName = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne; - var showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system; + var showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system + && widget.bubbleData.messageType != GetRoomResponseObjectMessageType.deletedComment; + var parent = widget.bubbleData.parent; var actorText = Text( widget.bubbleData.actorDisplayName, textAlign: TextAlign.start, @@ -288,6 +276,24 @@ class _ChatBubbleState extends State { children: [ GestureDetector( + onHorizontalDragStart: (details) { + _dragStartPosition = _position; + }, + onHorizontalDragUpdate: (details) { + if(!widget.bubbleData.isReplyable) return; + var dx = details.delta.dx - _dragStartPosition.dx; + setState(() { + _position = (_position.dx + dx).abs() > 30 ? Offset(_position.dx, 0) : Offset(_position.dx + dx, 0); + }); + }, + onHorizontalDragEnd: (DragEndDetails details) { + setState(() { + _position = const Offset(0, 0); + }); + if(widget.bubbleData.isReplyable) { + Provider.of(context, listen: false).setReferenceMessageId(widget.bubbleData.id, context, widget.chatData.token); + } + }, onLongPress: showOptionsDialog, onDoubleTap: showOptionsDialog, onTap: () { @@ -336,55 +342,76 @@ class _ChatBubbleState extends State { } }); }, - child: Bubble( - style: getStyle(), - child: Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.9, - minWidth: showActorDisplayName - ? actorText.size.width - : timeText.size.width + (widget.isSender ? widget.spacing + widget.timeIconSize : 0) + 3, - ), - child: Stack( + child: Transform.translate( + offset: _position, + child: Bubble( + style: getStyle(), + child: Column( children: [ - Padding( - padding: EdgeInsets.only(bottom: showBubbleTime ? 18 : 0, top: showActorDisplayName ? 18 : 0), - child: message.getWidget() - ), - Visibility( - visible: showActorDisplayName, - child: Positioned( - top: 0, - left: 0, - child: actorText + Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.9, + minWidth: showActorDisplayName + ? actorText.size.width + : timeText.size.width + (widget.isSender ? widget.spacing + widget.timeIconSize : 0) + 3, ), - ), - Visibility( - visible: showBubbleTime, - child: Positioned( - bottom: 0, - right: 0, - child: Row( - children: [ - timeText, - if(widget.isSender) ...[ - SizedBox(width: widget.spacing), - if(widget.isRead) - Icon(Icons.done_all_outlined, size: widget.timeIconSize, color: widget.timeIconColor) - else - Icon(Icons.done_outlined, size: widget.timeIconSize, color: widget.timeIconColor) - ] - ], - ) - ), - ), - Visibility( - visible: downloadProgress > 0, - child: Positioned( - bottom: 0, - right: 0, - left: 0, - child: LinearProgressIndicator(value: downloadProgress/100), + child: Stack( + children: [ + Visibility( + visible: showActorDisplayName, + child: Positioned( + top: 0, + left: 0, + child: actorText + ), + ), + Padding( + padding: EdgeInsets.only(bottom: showBubbleTime ? 18 : 0, top: showActorDisplayName ? 18 : 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if(parent != null && widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment) ...[ + AnswerReference( + context: context, + referenceMessage: parent, + selfId: widget.selfId, + ), + const SizedBox(height: 5), + ], + message.getWidget(), + ], + ), + ), + Visibility( + visible: showBubbleTime, + child: Positioned( + bottom: 0, + right: 0, + child: Row( + children: [ + timeText, + if(widget.isSender) ...[ + SizedBox(width: widget.spacing), + Icon( + widget.isRead ? Icons.done_all_outlined: Icons.done_outlined, + size: widget.timeIconSize, + color: widget.timeIconColor + ) + ] + ], + ) + ), + ), + Visibility( + visible: downloadProgress > 0, + child: Positioned( + bottom: 0, + right: 0, + left: 0, + child: LinearProgressIndicator(value: downloadProgress/100), + ), + ), + ], ), ), ], @@ -417,9 +444,7 @@ class _ChatBubbleState extends State { DeleteReactMessage( chatToken: widget.chatData.token, messageId: widget.bubbleData.id, - params: DeleteReactMessageParams( - e.key - ), + params: DeleteReactMessageParams(e.key), ).run().then((value) => widget.refetch(renew: true)); } else { @@ -427,11 +452,8 @@ class _ChatBubbleState extends State { ReactMessage( chatToken: widget.chatData.token, messageId: widget.bubbleData.id, - params: ReactMessageParams( - e.key - ) + params: ReactMessageParams(e.key) ).run().then((value) => widget.refetch(renew: true)); - } }, ), diff --git a/lib/view/pages/talk/components/chatBubbleStyles.dart b/lib/view/pages/talk/components/chatBubbleStyles.dart new file mode 100644 index 0000000..c640ca2 --- /dev/null +++ b/lib/view/pages/talk/components/chatBubbleStyles.dart @@ -0,0 +1,54 @@ +import 'package:bubble/bubble.dart'; +import 'package:flutter/material.dart'; + +import '../../../../theming/appTheme.dart'; + +extension ColorExtensions on Color { + Color invert() { + final r = 255 - red; + final g = 255 - green; + final b = 255 - blue; + + return Color.fromARGB((opacity * 255).round(), r, g, b); + } + + Color withWhite(int whiteValue) => Color.fromARGB(alpha, whiteValue, whiteValue, whiteValue); +} + +class ChatBubbleStyles { + final BuildContext context; + + ChatBubbleStyles(this.context); + + BubbleStyle getSystemStyle() => BubbleStyle( + color: AppTheme.isDarkMode(context) ? const Color(0xff182229) : Colors.white, + borderWidth: 1, + elevation: 2, + margin: const BubbleEdges.only(bottom: 20, top: 10), + alignment: Alignment.center, + ); + + BubbleStyle getRemoteStyle(bool seamless) { + var color = AppTheme.isDarkMode(context) ? const Color(0xff202c33) : Colors.white; + return BubbleStyle( + nip: BubbleNip.leftTop, + color: seamless ? Colors.transparent : color, + borderWidth: seamless ? 0 : 1, + elevation: seamless ? 0 : 1, + margin: const BubbleEdges.only(bottom: 10, left: 10, right: 50), + alignment: Alignment.topLeft, + ); + } + + BubbleStyle getSelfStyle(bool seamless) { + var color = AppTheme.isDarkMode(context) ? const Color(0xff005c4b) : const Color(0xffd3d3d3); + return BubbleStyle( + nip: BubbleNip.rightBottom, + color: seamless ? Colors.transparent : color, + borderWidth: seamless ? 0 : 1, + elevation: seamless ? 0 : 1, + margin: const BubbleEdges.only(bottom: 10, right: 10, left: 50), + alignment: Alignment.topRight, + ); + } +} \ No newline at end of file diff --git a/lib/view/pages/talk/components/chatTextfield.dart b/lib/view/pages/talk/components/chatTextfield.dart index 3628006..2da2f9a 100644 --- a/lib/view/pages/talk/components/chatTextfield.dart +++ b/lib/view/pages/talk/components/chatTextfield.dart @@ -15,10 +15,12 @@ import '../../../../storage/base/settingsProvider.dart'; import '../../../../widget/filePick.dart'; import '../../../../widget/focusBehaviour.dart'; import '../../files/filesUploadDialog.dart'; +import 'answerReference.dart'; class ChatTextfield extends StatefulWidget { final String sendToToken; - const ChatTextfield(this.sendToToken, {super.key}); + final String? selfId; + const ChatTextfield(this.sendToToken, {this.selfId, super.key}); @override State createState() => _ChatTextfieldState(); @@ -78,6 +80,8 @@ class _ChatTextfieldState extends State { void initState() { super.initState(); settings = Provider.of(context, listen: false); + Provider.of(context, listen: false).unsafeInternalSetReferenceMessageId = + settings.val().talkSettings.draftReplies[widget.sendToToken]; } @override @@ -91,102 +95,136 @@ class _ChatTextfieldState extends State { child: Container( padding: const EdgeInsets.only(left: 10, bottom: 3, top: 3, right: 10), width: double.infinity, - child: Row( - children: [ - GestureDetector( - onTap: (){ - showDialog(context: context, builder: (context) => SimpleDialog( + child: Column( + children: [ + Consumer( + builder: (context, data, child) { + if(data.getReferenceMessageId != null) { + var referenceMessage = data.getChatResponse.sortByTimestamp().where((element) => element.id == data.getReferenceMessageId).last; + return Row( children: [ - ListTile( - leading: const Icon(Icons.file_open), - title: const Text('Aus Dateien auswählen'), - onTap: () { - FilePick.documentPick().then(mediaUpload); - Navigator.of(context).pop(); - }, - ), - Visibility( - visible: !Platform.isIOS, - child: ListTile( - leading: const Icon(Icons.image), - title: const Text('Aus Gallerie auswählen'), - onTap: () { - FilePick.multipleGalleryPick().then((value) { - if(value != null) mediaUpload(value.map((e) => e.path).toList()); - }); - Navigator.of(context).pop(); - }, + Expanded( + child: AnswerReference( + context: context, + referenceMessage: referenceMessage, + selfId: widget.selfId, ), ), + IconButton( + onPressed: () => data.setReferenceMessageId(null, context, widget.sendToToken), + icon: const Icon(Icons.close_outlined), + padding: const EdgeInsets.only(left: 0), + ), ], - )); + ); + } else { + return const SizedBox.shrink(); + } }, - child: Material( - elevation: 5, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), + ), + Row( + children: [ + GestureDetector( + onTap: (){ + showDialog(context: context, builder: (context) => SimpleDialog( + children: [ + ListTile( + leading: const Icon(Icons.file_open), + title: const Text('Aus Dateien auswählen'), + onTap: () { + FilePick.documentPick().then(mediaUpload); + Navigator.of(context).pop(); + }, + ), + Visibility( + visible: !Platform.isIOS, + child: ListTile( + leading: const Icon(Icons.image), + title: const Text('Aus Gallerie auswählen'), + onTap: () { + FilePick.multipleGalleryPick().then((value) { + if(value != null) mediaUpload(value.map((e) => e.path).toList()); + }); + Navigator.of(context).pop(); + }, + ), + ), + ], + )); + }, + child: Material( + elevation: 5, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + child: Container( + height: 30, + width: 30, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(30), + ), + child: const Icon(Icons.attach_file_outlined, color: Colors.white, size: 20, ), + ), + ) ), - child: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(30), + const SizedBox(width: 15), + Expanded( + child: TextField( + autocorrect: true, + textCapitalization: TextCapitalization.sentences, + controller: _textBoxController, + maxLines: 7, + minLines: 1, + decoration: const InputDecoration( + hintText: 'Nachricht schreiben...', + border: InputBorder.none, + ), + onChanged: (String text) { + if(text.trim().toLowerCase() == 'marbot marbot marbot') { + var newText = 'Roboter sind cool und so, aber Marbots sind besser!'; + _textBoxController.text = newText; + text = newText; + } + setDraft(text); + }, + onTapOutside: (PointerDownEvent event) => FocusBehaviour.textFieldTapOutside(context), ), - child: const Icon(Icons.attach_file_outlined, color: Colors.white, size: 20, ), ), - ) - ), - const SizedBox(width: 15), - Expanded( - child: TextField( - autocorrect: true, - textCapitalization: TextCapitalization.sentences, - controller: _textBoxController, - maxLines: 7, - minLines: 1, - decoration: const InputDecoration( - hintText: 'Nachricht schreiben...', - border: InputBorder.none, - ), - onChanged: (String text) { - if(text.trim().toLowerCase() == 'marbot marbot marbot') { - var newText = 'Roboter sind cool und so, aber Marbots sind besser!'; - _textBoxController.text = newText; - text = newText; - } - setDraft(text); - }, - onTapOutside: (PointerDownEvent event) => FocusBehaviour.textFieldTapOutside(context), - ), - ), - const SizedBox(width: 15), - FloatingActionButton( - mini: true, - onPressed: (){ - if(_textBoxController.text.isEmpty) return; - if(isLoading) return; + const SizedBox(width: 15), + FloatingActionButton( + mini: true, + onPressed: () { + if(_textBoxController.text.isEmpty) return; + if(isLoading) return; - setState(() { - isLoading = true; - }); - SendMessage(widget.sendToToken, SendMessageParams(_textBoxController.text)).run().then((value) { - _query(); - setState(() { - isLoading = false; - }); - _textBoxController.text = ''; - setDraft(''); - }); - }, - backgroundColor: Theme.of(context).primaryColor, - elevation: 5, - child: isLoading - ? Container(padding: const EdgeInsets.all(10), child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) - : const Icon(Icons.send, color: Colors.white, size: 18), + setState(() { + isLoading = true; + }); + SendMessage(widget.sendToToken, SendMessageParams( + _textBoxController.text, + replyTo: Provider.of(context, listen: false).getReferenceMessageId.toString() + )).run().then((value) { + _query(); + setState(() { + isLoading = false; + }); + _textBoxController.text = ''; + setDraft(''); + Provider.of(context, listen: false).setReferenceMessageId(null, context, widget.sendToToken); + }); + }, + backgroundColor: Theme.of(context).primaryColor, + elevation: 5, + child: isLoading + ? Container(padding: const EdgeInsets.all(10), child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) + : const Icon(Icons.send, color: Colors.white, size: 18), + ), + ], ), ], ), + ), ), ], diff --git a/lib/view/settings/defaultSettings.dart b/lib/view/settings/defaultSettings.dart index 20ce747..1ff2d77 100644 --- a/lib/view/settings/defaultSettings.dart +++ b/lib/view/settings/defaultSettings.dart @@ -29,6 +29,7 @@ class DefaultSettings { sortFavoritesToTop: true, sortUnreadToTop: false, drafts: {}, + draftReplies: {}, ), fileSettings: FileSettings( sortFoldersToTop: true,