338 lines
13 KiB
Dart
338 lines
13 KiB
Dart
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<ChatBubble> createState() => _ChatBubbleState();
|
|
}
|
|
|
|
class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateMixin {
|
|
late ChatMessage message;
|
|
double downloadProgress = 0;
|
|
Future<DownloaderCore>? 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<ChatBloc>().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<Widget>((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() ?? [],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|