import 'package:better_open_file/better_open_file.dart'; import 'package:bubble/bubble.dart'; import 'package:flowder/flowder.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:jiffy/jiffy.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 '../../../theming/appTheme.dart'; import '../../../widget/debug/debugTile.dart'; import '../files/fileElement.dart'; import 'chatMessage.dart'; class ChatBubble extends StatefulWidget { final BuildContext context; final bool isSender; final GetChatResponseObject bubbleData; final GetRoomResponseObject chatData; final void Function({bool renew}) refetch; const ChatBubble({ required this.context, required this.isSender, required this.bubbleData, required this.chatData, required this.refetch, Key? key}) : super(key: key); @override State createState() => _ChatBubbleState(); } class _ChatBubbleState extends State { BubbleStyle getSystemStyle() { return 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, ); } late ChatMessage message; double downloadProgress = 0; Future? downloadCore; Size _textSize(String text, TextStyle style) { final TextPainter textPainter = TextPainter( text: TextSpan(text: text, style: style), maxLines: 1, textDirection: TextDirection.ltr) ..layout(minWidth: 0, maxWidth: double.infinity); return textPainter.size; } BubbleStyle getStyle() { if(widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment) { if(widget.isSender) { return getSelfStyle(message.containsFile); } else { return getRemoteStyle(message.containsFile); } } else { return getSystemStyle(); } } @override Widget build(BuildContext context) { message = ChatMessage(originalMessage: widget.bubbleData.message, originalData: widget.bubbleData.messageParameters); bool showActorDisplayName = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne; bool showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system; var actorTextStyle = TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold); return Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, textDirection: TextDirection.ltr, crossAxisAlignment: CrossAxisAlignment.end, children: [ GestureDetector( child: Bubble( style: getStyle(), child: Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.9, minWidth: showActorDisplayName ? _textSize(widget.bubbleData.actorDisplayName, actorTextStyle).width : 30, ), child: Stack( 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: Text( widget.bubbleData.actorDisplayName, textAlign: TextAlign.start, style: actorTextStyle, ), ), ), Visibility( visible: showBubbleTime, child: Positioned( bottom: 0, right: 0, child: Text( Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: "HH:mm"), textAlign: TextAlign.end, style: const TextStyle(color: Colors.grey, fontSize: 12), ), ), ), Visibility( visible: downloadProgress > 0, child: Positioned( top: 0, left: 0, right: 0, bottom: 0, child: Stack( children: [ const Center(child: Icon(Icons.download)), const Center(child: CircularProgressIndicator(color: Colors.white)), Center(child: CircularProgressIndicator(value: downloadProgress/100)), ], ) ), ), ], ), ), ), onLongPress: () { showDialog(context: context, builder: (context) { List commonReactions = ["😆", "👍", "👎", "❤️", "💔", "😍"]; return SimpleDialog( 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)), ), ], ), Visibility( visible: !message.containsFile && widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment, 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: !widget.isSender && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne, child: ListTile( leading: const Icon(Icons.sms_outlined), title: Text("Private Nachricht an '${widget.bubbleData.actorDisplayName}'"), onTap: () => {}, ), ), Visibility( visible: widget.isSender, 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(widget.bubbleData.toJson()).asTile(context), ], ); }); }, onTap: () { if(message.file == null) return; if(downloadProgress > 0) { showDialog(context: context, builder: (context) { return 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) { return AlertDialog( content: Text(result.message), ); }); } }); }, ), 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: 5, right: 5), child: Wrap( alignment: widget.isSender ? WrapAlignment.end : WrapAlignment.start, crossAxisAlignment: WrapCrossAlignment.start, children: widget.bubbleData.reactions?.entries.map((e) { bool 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() ?? [], ), ), ), ), ], ); } }