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'; import 'package:jiffy/jiffy.dart'; import '../../../../extensions/text.dart'; import 'package:provider/provider.dart'; import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../../api/marianumcloud/talk/deleteMessage/deleteMessage.dart'; import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessage.dart'; import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessageParams.dart'; 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 '../../../../widget/debug/debugTile.dart'; import '../../files/fileElement.dart'; import 'answerReference.dart'; import 'chatBubbleStyles.dart'; import 'chatMessage.dart'; import '../messageReactions.dart'; class ChatBubble extends StatefulWidget { final BuildContext context; final bool isSender; final GetChatResponseObject bubbleData; final GetRoomResponseObject chatData; final bool isRead; final String? selfId; final double spacing = 3; final double timeIconSize = 11; final Color timeIconColor = Colors.grey; final void Function({bool renew}) refetch; const ChatBubble({ required this.context, required this.isSender, required this.bubbleData, required this.chatData, required this.refetch, this.isRead = false, this.selfId, super.key}); @override State createState() => _ChatBubbleState(); } 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 styles.getSelfStyle(false); } else { return styles.getRemoteStyle(false); } } else { return styles.getSystemStyle(); } } void showOptionsDialog() { showDialog(context: context, builder: (context) { var commonReactions = ['👍', '👎', '😆', '❤️', '👀']; var canReact = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment; return SimpleDialog( children: [ Visibility( visible: canReact, child: Column( mainAxisSize: MainAxisSize.min, children: [ Wrap( alignment: WrapAlignment.center, children: [ ...commonReactions.map((e) => TextButton( style: TextButton.styleFrom( padding: EdgeInsets.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, minimumSize: const Size(40, 40) ), onPressed: () { Navigator.of(context).pop(); ReactMessage( chatToken: widget.chatData.token, messageId: widget.bubbleData.id, params: ReactMessageParams(e), ).run().then((value) => widget.refetch(renew: true)); }, child: Text(e), ), ), IconButton( onPressed: () { showDialog(context: context, builder: (context) => AlertDialog( contentPadding: const EdgeInsets.all(15), titlePadding: const EdgeInsets.only(left: 6, top: 15), title: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ IconButton( onPressed: () { Navigator.of(context).pop(); }, icon: const Icon(Icons.arrow_back), ), const SizedBox(width: 10), const Text('Reagieren'), ], ), content: SizedBox( width: 256, height: 270, child: Column( children: [ emojis.EmojiPicker( config: emojis.Config( height: 256, swapCategoryAndBottomBar: true, emojiViewConfig: emojis.EmojiViewConfig( backgroundColor: Theme.of(context).canvasColor, recentsLimit: 67, emojiSizeMax: 25, noRecents: const Text('Keine zuletzt verwendeten Emojis'), columns: 7, ), bottomActionBarConfig: const emojis.BottomActionBarConfig( enabled: false, ), categoryViewConfig: emojis.CategoryViewConfig( backgroundColor: Theme.of(context).hoverColor, iconColorSelected: Theme.of(context).primaryColor, indicatorColor: Theme.of(context).primaryColor, ), searchViewConfig: emojis.SearchViewConfig( backgroundColor: Theme.of(context).dividerColor, buttonColor: Theme.of(context).dividerColor, hintText: 'Suchen', buttonIconColor: Colors.white, ), ), onEmojiSelected: (emojis.Category? category, emojis.Emoji emoji) { Navigator.of(context).pop(); Navigator.of(context).pop(); ReactMessage( chatToken: widget.chatData.token, messageId: widget.bubbleData.id, params: ReactMessageParams(emoji.emoji), ).run().then((value) => widget.refetch(renew: true)); }, ), ], ), ), )); }, style: IconButton.styleFrom( padding: EdgeInsets.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, minimumSize: const Size(40, 40), ), icon: const Icon(Icons.add_circle_outline_outlined), ), ], ), const Divider(), ], ), ), 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( leading: const Icon(Icons.emoji_emotions_outlined), title: const Text('Reaktionen'), onTap: () { Navigator.of(context).push(MaterialPageRoute(builder: (context) => MessageReactions( token: widget.chatData.token, messageId: widget.bubbleData.id, ))); }, ), ), Visibility( visible: !message.containsFile, child: ListTile( leading: const Icon(Icons.copy), title: const Text('Nachricht kopieren'), onTap: () => { Clipboard.setData(ClipboardData(text: widget.bubbleData.message)), Navigator.of(context).pop(), }, ), ), Visibility( visible: !kReleaseMode && !widget.isSender && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne, child: ListTile( leading: const Icon(Icons.sms_outlined), title: Text("Private Nachricht an '${widget.bubbleData.actorDisplayName}'"), onTap: () => { Navigator.of(context).pop() }, ), ), Visibility( visible: widget.isSender && DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).add(const Duration(hours: 6)).isAfter(DateTime.now()), child: ListTile( leading: const Icon(Icons.delete_outline), title: const Text('Nachricht löschen'), onTap: () { DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) { Provider.of(context, listen: false).run(); Navigator.of(context).pop(); }); }, ), ), DebugTile(context).jsonData(widget.bubbleData.toJson()), ], ); }); } @override 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 && widget.bubbleData.messageType != GetRoomResponseObjectMessageType.deletedComment; var parent = widget.bubbleData.parent; var actorText = Text( widget.bubbleData.actorDisplayName, textAlign: TextAlign.start, overflow: TextOverflow.ellipsis, style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold), ); var timeText = Text( Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: 'HH:mm'), textAlign: TextAlign.end, style: TextStyle(color: widget.timeIconColor, fontSize: widget.timeIconSize), ); return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, textDirection: TextDirection.ltr, crossAxisAlignment: CrossAxisAlignment.end, 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: () { if(message.file == null) return; if(downloadProgress > 0) { showDialog(context: context, builder: (context) => AlertDialog( title: const Text('Download abbrechen?'), content: const Text('Möchtest du den Download abbrechen?'), actions: [ TextButton(onPressed: () { Navigator.of(context).pop(); }, child: const Text('Nein')), TextButton(onPressed: () { downloadCore?.then((value) { if(!value.isCancelled) value.cancel(); Navigator.of(context).pop(); }); setState(() { downloadProgress = 0; downloadCore = null; }); }, child: const Text('Ja, Abbrechen')) ], )); return; } downloadProgress = 1; downloadCore = FileElement.download(context, message.file!.path!, message.file!.name, (progress) { if(progress > 1) { setState(() { downloadProgress = progress; }); } }, (result) { setState(() { downloadProgress = 0; }); if(result.type != ResultType.done) { showDialog(context: context, builder: (context) => AlertDialog( content: Text(result.message), )); } }); }, child: Transform.translate( offset: _position, child: Bubble( style: getStyle(), child: Column( children: [ 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( 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), ), ), ], ), ), ], ), ), ), ), Visibility( visible: widget.bubbleData.reactions != null, child: Transform.translate( offset: const Offset(0, -10), child: Container( width: MediaQuery.of(context).size.width, margin: const EdgeInsets.only(left: 15, right: 15), child: Wrap( alignment: widget.isSender ? WrapAlignment.end : WrapAlignment.start, crossAxisAlignment: WrapCrossAlignment.start, children: widget.bubbleData.reactions?.entries.map((e) { var hasSelfReacted = widget.bubbleData.reactionsSelf?.contains(e.key) ?? false; return Container( margin: const EdgeInsets.only(right: 2.5, left: 2.5), child: ActionChip( label: Text('${e.key} ${e.value}'), visualDensity: const VisualDensity(vertical: VisualDensity.minimumDensity, horizontal: VisualDensity.minimumDensity), padding: EdgeInsets.zero, backgroundColor: hasSelfReacted ? Theme.of(context).primaryColor : null, onPressed: () { if(hasSelfReacted) { // Delete existing reaction DeleteReactMessage( chatToken: widget.chatData.token, messageId: widget.bubbleData.id, params: DeleteReactMessageParams(e.key), ).run().then((value) => widget.refetch(renew: true)); } else { // Add reaction ReactMessage( chatToken: widget.chatData.token, messageId: widget.bubbleData.id, params: ReactMessageParams(e.key) ).run().then((value) => widget.refetch(renew: true)); } }, ), ); }).toList() ?? [], ), ), ), ), ], ); } }