import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:solian/models/message.dart'; import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:timeago/timeago.dart' show format; import 'package:url_launcher/url_launcher_string.dart'; class ChatMessage extends StatelessWidget { final Message item; final bool isContentPreviewing; final bool isReply; final bool isMerged; final bool isHasMerged; const ChatMessage({ super.key, required this.item, this.isContentPreviewing = false, this.isMerged = false, this.isHasMerged = false, this.isReply = false, }); Future decodeContent(Map content) async { String? text; if (item.type == 'm.text') { switch (content['algorithm']) { case 'plain': text = content['value']; default: throw Exception('Unsupported algorithm'); } } return text; } Widget buildContent() { final hasAttachment = item.attachments?.isNotEmpty ?? false; return FutureBuilder( future: decodeContent(item.content), builder: (context, snapshot) { if (!snapshot.hasData) { return Opacity( opacity: 0.8, child: Row( children: [ const Icon(Icons.more_horiz), const SizedBox(width: 4), Text('messageDecoding'.tr) ], ), ).animate(onPlay: (c) => c.repeat()).fade(begin: 0, end: 1); } else if (snapshot.hasError) { return Opacity( opacity: 0.9, child: Row( children: [ const Icon(Icons.close), const SizedBox(width: 4), Text( 'messageDecodeFailed' .trParams({'message': snapshot.error.toString()}), ) ], ), ); } return Markdown( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), data: snapshot.data ?? '', padding: const EdgeInsets.all(0), onTapLink: (text, href, title) async { if (href == null) return; await launchUrlString( href, mode: LaunchMode.externalApplication, ); }, ).paddingOnly( left: 12, right: 12, top: 2, bottom: hasAttachment ? 4 : 0, ); }, ); } @override Widget build(BuildContext context) { Widget widget; if (isContentPreviewing) { widget = buildContent(); } else if (isMerged) { widget = Column( children: [ buildContent().paddingOnly(left: 52), if (item.attachments?.isNotEmpty ?? false) AttachmentList( key: Key('m${item.uuid}attachments'), parentId: item.uuid, attachmentsId: item.attachments ?? List.empty(), ).paddingSymmetric(vertical: 4), ], ); } else if (isReply) { widget = Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Transform.scale( scaleX: -1, child: const FaIcon(FontAwesomeIcons.reply, size: 14), ), const SizedBox(width: 12), AccountAvatar(content: item.sender.account.avatar, radius: 8), const SizedBox(width: 4), Text( item.sender.account.nick, style: const TextStyle(fontWeight: FontWeight.bold), ), Expanded(child: buildContent()), ], ); } else { widget = Column( key: Key('m${item.uuid}'), children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ AccountAvatar(content: item.sender.account.avatar), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( item.sender.account.nick, style: const TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(width: 4), Text(format(item.createdAt, locale: 'en_short')) ], ).paddingSymmetric(horizontal: 12), buildContent(), ], ), ), ], ).paddingSymmetric(horizontal: 12), if (item.attachments?.isNotEmpty ?? false) AttachmentList( key: Key('m${item.uuid}attachments'), parentId: item.uuid, attachmentsId: item.attachments ?? List.empty(), ).paddingSymmetric(vertical: 4), ], ); } if (item.isSending) { return Opacity(opacity: 0.65, child: widget); } else { return widget; } } }