348 lines
11 KiB
Dart
348 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
|
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
|
import '../../../../extensions/date_time.dart';
|
|
import '../../../../extensions/text.dart';
|
|
import '../../../../routing/app_routes.dart';
|
|
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
|
import '../../../../utils/download_manager.dart';
|
|
import '../../../../widget/confirm_dialog.dart';
|
|
import '../../../../widget/info_dialog.dart';
|
|
import '../data/chat_bubble_styles.dart';
|
|
import '../data/chat_message.dart';
|
|
import 'answer_reference.dart';
|
|
import 'bubble.dart';
|
|
import 'chat_bubble_poll.dart';
|
|
import 'chat_bubble_reactions.dart';
|
|
import 'chat_message_options_dialog.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;
|
|
DownloadJob? _job;
|
|
|
|
Offset _position = Offset.zero;
|
|
Offset _dragStartPosition = Offset.zero;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final filePath = widget.bubbleData.messageParameters?['file']?.path;
|
|
if (filePath != null) _attachJob(DownloadManager.instance.jobFor(filePath));
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_detachJob();
|
|
super.dispose();
|
|
}
|
|
|
|
void _attachJob(DownloadJob? job) {
|
|
_job = job;
|
|
if (job == null) return;
|
|
job.status.addListener(_onStatusChange);
|
|
if (job.isFinished) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) => _onStatusChange());
|
|
}
|
|
}
|
|
|
|
void _detachJob() {
|
|
_job?.status.removeListener(_onStatusChange);
|
|
_job = null;
|
|
}
|
|
|
|
void _onStatusChange() {
|
|
if (!mounted) return;
|
|
final job = _job;
|
|
if (job == null) return;
|
|
final status = job.status.value;
|
|
if (status is DownloadDone) {
|
|
DownloadManager.instance.clear(job.remotePath);
|
|
_detachJob();
|
|
AppRoutes.openFileViewer(context, status.localPath);
|
|
setState(() {});
|
|
} else if (status is DownloadFailed) {
|
|
final message = status.message;
|
|
DownloadManager.instance.clear(job.remotePath);
|
|
_detachJob();
|
|
setState(() {});
|
|
InfoDialog.show(context, message, title: 'Download fehlgeschlagen');
|
|
} else if (status is DownloadCancelled) {
|
|
DownloadManager.instance.clear(job.remotePath);
|
|
_detachJob();
|
|
setState(() {});
|
|
} else {
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
Future<void> _startFileDownload() async {
|
|
final file = message.file;
|
|
final filePath = file?.path;
|
|
if (file == null || filePath == null) return;
|
|
final job = await DownloadManager.instance.start(remotePath: filePath, name: file.name);
|
|
if (!mounted) return;
|
|
if (_job == job) return;
|
|
_detachJob();
|
|
_attachJob(job);
|
|
setState(() {});
|
|
}
|
|
|
|
void _confirmCancel() {
|
|
ConfirmDialog(
|
|
title: 'Download abbrechen?',
|
|
content: 'Möchtest du den Download abbrechen?',
|
|
confirmButton: 'Ja, Abbrechen',
|
|
cancelButton: 'Nein',
|
|
onConfirm: () => _job?.cancel(),
|
|
).asDialog(context);
|
|
}
|
|
|
|
BubbleStyle _getStyle() {
|
|
final styles = ChatBubbleStyles(context);
|
|
if (widget.bubbleData.messageType != GetRoomResponseObjectMessageType.comment) {
|
|
return styles.getSystemStyle();
|
|
}
|
|
return widget.isSender ? styles.getSelfStyle(false) : styles.getRemoteStyle(false);
|
|
}
|
|
|
|
void _showOptionsDialog() => showChatMessageOptionsDialog(
|
|
context,
|
|
chatData: widget.chatData,
|
|
bubbleData: widget.bubbleData,
|
|
isSender: widget.isSender,
|
|
onRefetch: widget.refetch,
|
|
);
|
|
|
|
void _onTap() {
|
|
final obj = message.originalData?['object'];
|
|
if (obj?.type == RichObjectStringObjectType.talkPoll) {
|
|
showChatBubblePollDialog(
|
|
context,
|
|
chatToken: widget.chatData.token,
|
|
messageToken: widget.bubbleData.token,
|
|
pollId: int.parse(obj!.id),
|
|
pollName: obj.name,
|
|
);
|
|
return;
|
|
}
|
|
if (message.file == null) return;
|
|
if (_job?.status.value is DownloadInProgress) {
|
|
_confirmCancel();
|
|
} else {
|
|
_startFileDownload();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
message = ChatMessage(originalMessage: widget.bubbleData.message, originalData: widget.bubbleData.messageParameters);
|
|
final showActorDisplayName = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment
|
|
&& widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne;
|
|
final showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system
|
|
&& widget.bubbleData.messageType != GetRoomResponseObjectMessageType.deletedComment;
|
|
|
|
final parent = widget.bubbleData.parent;
|
|
final actorText = Text(
|
|
widget.bubbleData.actorDisplayName,
|
|
textAlign: TextAlign.start,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
|
|
);
|
|
|
|
final timeText = Text(
|
|
DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).formatHm(),
|
|
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: (_) => _dragStartPosition = _position,
|
|
onHorizontalDragUpdate: (details) {
|
|
if (!widget.bubbleData.isReplyable) return;
|
|
final dx = details.delta.dx - _dragStartPosition.dx;
|
|
setState(() {
|
|
_position = (_position.dx + dx).abs() > 60
|
|
? Offset(_position.dx, 0)
|
|
: Offset(_position.dx + dx, 0);
|
|
});
|
|
},
|
|
onHorizontalDragEnd: (_) {
|
|
final isAction = _position.dx.abs() > 50;
|
|
setState(() => _position = Offset.zero);
|
|
if (widget.bubbleData.isReplyable && isAction) {
|
|
context.read<ChatBloc>().setReferenceMessageId(widget.bubbleData.id);
|
|
}
|
|
},
|
|
onLongPress: _showOptionsDialog,
|
|
onDoubleTap: _showOptionsDialog,
|
|
onTap: _onTap,
|
|
child: Transform.translate(
|
|
offset: _position,
|
|
child: Bubble(
|
|
style: _getStyle(),
|
|
child: _BubbleContent(
|
|
actorText: actorText,
|
|
timeText: timeText,
|
|
messageWidget: message.getWidget(),
|
|
parent: parent,
|
|
bubbleData: widget.bubbleData,
|
|
isSender: widget.isSender,
|
|
isRead: widget.isRead,
|
|
selfId: widget.selfId,
|
|
spacing: widget.spacing,
|
|
timeIconSize: widget.timeIconSize,
|
|
timeIconColor: widget.timeIconColor,
|
|
showActorDisplayName: showActorDisplayName,
|
|
showBubbleTime: showBubbleTime,
|
|
downloadJob: _job,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
ChatBubbleReactions(
|
|
bubbleData: widget.bubbleData,
|
|
chatData: widget.chatData,
|
|
isSender: widget.isSender,
|
|
onChanged: widget.refetch,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BubbleContent extends StatelessWidget {
|
|
final Text actorText;
|
|
final Text timeText;
|
|
final Widget messageWidget;
|
|
final GetChatResponseObject? parent;
|
|
final GetChatResponseObject bubbleData;
|
|
final bool isSender;
|
|
final bool isRead;
|
|
final String? selfId;
|
|
final double spacing;
|
|
final double timeIconSize;
|
|
final Color timeIconColor;
|
|
final bool showActorDisplayName;
|
|
final bool showBubbleTime;
|
|
final DownloadJob? downloadJob;
|
|
|
|
const _BubbleContent({
|
|
required this.actorText,
|
|
required this.timeText,
|
|
required this.messageWidget,
|
|
required this.parent,
|
|
required this.bubbleData,
|
|
required this.isSender,
|
|
required this.isRead,
|
|
required this.selfId,
|
|
required this.spacing,
|
|
required this.timeIconSize,
|
|
required this.timeIconColor,
|
|
required this.showActorDisplayName,
|
|
required this.showBubbleTime,
|
|
required this.downloadJob,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Container(
|
|
constraints: BoxConstraints(
|
|
maxWidth: MediaQuery.of(context).size.width * 0.9,
|
|
minWidth: showActorDisplayName
|
|
? actorText.size.width
|
|
: timeText.size.width + (isSender ? spacing + timeIconSize : 0) + 3,
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
if (showActorDisplayName)
|
|
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 && bubbleData.messageType == GetRoomResponseObjectMessageType.comment) ...[
|
|
AnswerReference(
|
|
context: context,
|
|
referenceMessage: parent!,
|
|
selfId: selfId,
|
|
),
|
|
const SizedBox(height: 5),
|
|
],
|
|
messageWidget,
|
|
],
|
|
),
|
|
),
|
|
if (showBubbleTime)
|
|
Positioned(
|
|
bottom: 0,
|
|
right: 0,
|
|
child: Row(
|
|
children: [
|
|
timeText,
|
|
if (isSender) ...[
|
|
SizedBox(width: spacing),
|
|
Icon(
|
|
isRead ? Icons.done_all_outlined : Icons.done_outlined,
|
|
size: timeIconSize,
|
|
color: timeIconColor,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
if (downloadJob?.status.value is DownloadInProgress)
|
|
Positioned(
|
|
bottom: 0,
|
|
right: 0,
|
|
left: 0,
|
|
child: LinearProgressIndicator(
|
|
value: () {
|
|
final s = downloadJob!.status.value as DownloadInProgress;
|
|
return s.percent <= 0 ? null : s.percent / 100;
|
|
}(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|