import 'package:bubble/bubble.dart'; import 'package:flowder/flowder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:jiffy/jiffy.dart'; import 'package:open_filex/open_filex.dart'; import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessage.dart'; import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessageParams.dart'; import '../../../../api/marianumcloud/talk/getPoll/getPollState.dart'; import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart'; import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart'; import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart'; import '../../../../extensions/text.dart'; import '../../../../state/app/modules/chat/bloc/chat_bloc.dart'; import '../../../../widget/async_action_button.dart'; import '../../../../widget/loading_spinner.dart'; import '../../files/widgets/file_element.dart'; import '../data/chat_bubble_styles.dart'; import '../data/chat_message.dart'; import 'answer_reference.dart'; import 'chat_message_options_dialog.dart'; import 'poll_options_list.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() { showChatMessageOptionsDialog( context, chatData: widget.chatData, bubbleData: widget.bubbleData, isSender: widget.isSender, onRefetch: widget.refetch, ); } @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() > 60 ? Offset(_position.dx, 0) : Offset(_position.dx + dx, 0); }); }, onHorizontalDragEnd: (DragEndDetails details) { var isAction = _position.dx.abs() > 50; setState(() { _position = const Offset(0, 0); }); if(widget.bubbleData.isReplyable && isAction) { context.read().setReferenceMessageId(widget.bubbleData.id); } }, onLongPress: showOptionsDialog, onDoubleTap: showOptionsDialog, onTap: () { if(message.originalData?['object']?.type == RichObjectStringObjectType.talkPoll) { var pollId = int.parse(message.originalData!['object']!.id); var pollState = GetPollState(token: widget.bubbleData.token, pollId: pollId).run(); showDialog(context: context, builder: (context) => AlertDialog( title: Text(message.originalData!['object']!.name, overflow: TextOverflow.ellipsis), content: FutureBuilder( future: pollState, builder: (context, snapshot) { if(snapshot.connectionState == ConnectionState.waiting) return const Column(mainAxisSize: MainAxisSize.min, children: [LoadingSpinner()]); var pollData = snapshot.data!.data; return SingleChildScrollView( child: PollOptionsList( pollData: pollData, chatToken: widget.chatData.token, ), ); } ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Zurück') ), ], )); } 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(); if (!context.mounted) return; Navigator.of(context).pop(); }); setState(() { downloadProgress = 0; downloadCore = null; }); }, child: const Text('Ja, Abbrechen')) ], )); return; } setState(() { 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 == 1 ? null : 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: () { runWithErrorDialog(context, () async { if (hasSelfReacted) { await DeleteReactMessage( chatToken: widget.chatData.token, messageId: widget.bubbleData.id, params: DeleteReactMessageParams(e.key), ).run(); } else { await ReactMessage( chatToken: widget.chatData.token, messageId: widget.bubbleData.id, params: ReactMessageParams(e.key), ).run(); } widget.refetch(renew: true); }); }, ), ); }).toList() ?? [], ), ), ), ), ], ); } }