import 'dart:convert'; import 'package:extended_image/extended_image.dart' show ExtendedNetworkImageProvider; import 'package:extended_text/extended_text.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_ninepatch_image/flutter_ninepatch_image.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../app_localizations.dart'; import '../../../chatvibe_core/constants/at_app_colors.dart'; import '../../../chatvibe_core/constants/at_global_config.dart'; import '../../../chatvibe_core/constants/at_room_msg_type.dart'; import '../../../chatvibe_core/constants/at_screen.dart'; import '../../../chatvibe_core/routes/at_fluro_navigator.dart'; import '../../../chatvibe_core/utilities/at_room_utils.dart'; import '../../../chatvibe_data/models/enum/at_activity_reward_props_type.dart'; import '../../../chatvibe_data/models/enum/at_props_type.dart'; import '../../../chatvibe_data/models/enum/at_room_roles_type.dart'; import '../../../chatvibe_data/sources/repositories/user_repository_impl.dart'; import '../../../chatvibe_domain/models/res/gift_res.dart'; import '../../../chatvibe_domain/models/res/login_res.dart'; import '../../../chatvibe_domain/models/res/mic_res.dart'; import '../../../chatvibe_domain/usecases/at_case.dart'; import '../../../chatvibe_features/index/main_route.dart'; import '../../components/at_compontent.dart'; import '../../components/at_tts.dart'; import '../../components/text/at_text.dart'; ///消息item class MsgItem extends StatefulWidget { static final Map> roomUserFutureCache = >{}; static final Set roomUserRefreshInFlight = {}; static final Set roomUserFullInfoLoaded = {}; static final Map badgeImageProviderCache = {}; static final Map chatBoxImageProviderCache = {}; static void clearRoomUserCaches({bool clearBadgeCache = false}) { roomUserFutureCache.clear(); roomUserRefreshInFlight.clear(); roomUserFullInfoLoaded.clear(); chatBoxImageProviderCache.clear(); if (clearBadgeCache) { badgeImageProviderCache.clear(); } } final Function(ChatVibeUserProfile? user) onClick; final Msg msg; MsgItem({Key? key, required this.msg, required this.onClick}) : super(key: key); @override _MsgItemState createState() => _MsgItemState(); } class _MsgItemState extends State { static const int _maxRoomUserFutureCacheSize = 256; static const int _maxBadgeImageProviderCacheSize = 256; static const int _maxChatBoxImageProviderCacheSize = 64; static const Duration _badgeImageCacheMaxAge = Duration(days: 7); static const Duration _chatBoxImageCacheMaxAge = Duration(days: 7); @override Widget build(BuildContext context) { //gift // if (msg.type == Roomtype.gift.code) { // return _buildGiftMsg(context); // } ///系统提示 if (widget.msg.type == ATRoomMsgType.systemTips) { return _systemTipsRoomItem1(context); } ///进入房间 if (widget.msg.type == ATRoomMsgType.joinRoom) { return _enterRoomItem1(context); } ///礼物 if (widget.msg.type == ATRoomMsgType.gift) { return _buildGiftMsg1(context); } ///火箭用户中奖 if (widget.msg.type == ATRoomMsgType.rocketRewardUser) { return _buildRocketRewardMsg1(context); } ///幸运礼物 if (widget.msg.type == ATRoomMsgType.gameLuckyGift) { return _buildGameLuckyGiftMsg1(context); } ///幸运礼物 if (widget.msg.type == ATRoomMsgType.gameLuckyGift_5) { return _buildGameLuckyGiftMsg_5(context); } ///房间身份变动 if (widget.msg.type == ATRoomMsgType.roomRoleChange) { return _buildRoleChangeMsg1(context); } ///踢出房间 if (widget.msg.type == ATRoomMsgType.qcfj) { widget.msg.msg = ATAppLocalizations.of(context)!.kickRoomTips; } ///图片消息 if (widget.msg.type == ATRoomMsgType.image) { return _buildImageMsg1(context); } ///掷骰子 if (widget.msg.type == ATRoomMsgType.roomDice) { return _buildDiceMsg1(context); } ///石头剪刀布 if (widget.msg.type == ATRoomMsgType.roomRPS) { return _buildRPSMsg1(context); } ///幸运数字 if (widget.msg.type == ATRoomMsgType.roomLuckNumber) { return _buildLuckNumberMsg1(context); } if (widget.msg.user != null) { PropsResources? chatBox = widget.msg.user?.getChatBox(); return Container( alignment: AlignmentDirectional.topStart, child: Container( padding: EdgeInsets.symmetric( horizontal: width(5.w), vertical: width(4), ), margin: EdgeInsets.symmetric( horizontal: width(10), ).copyWith(bottom: 6.w), child: Column( children: [ _buildUserInfoLine(context), SizedBox(height: 6.w), Row( children: [ Flexible( // 使用Flexible替代Expanded child: GestureDetector( child: chatBox != null ? Container( constraints: BoxConstraints( maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 ), padding: EdgeInsets.symmetric(horizontal: 10.w), margin: EdgeInsetsDirectional.only( start: 34.w, ).copyWith(bottom: 1.w), child: NinePatchImage( scale: 3, alignment: Alignment.center, imageProvider: _getChatBoxImageProvider( chatBox.expand, ), sliceCachedKey: _buildNinePatchCacheKey( chatBox.expand, ), child: ExtendedText( widget.msg?.msg ?? '', style: TextStyle( fontSize: sp(13), color: Color( _msgColor(widget.msg.type ?? ""), ), fontWeight: FontWeight.w500, ), softWrap: true, // 自动换行 specialTextSpanBuilder: AtTextSpanBuilder( onTapCall: (userId) {}, ), ), ), ) : Container( constraints: BoxConstraints( maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 ), decoration: BoxDecoration( color: Color(0xffB6A1DE).withOpacity(0.2), borderRadius: BorderRadius.only( topRight: Radius.circular(15.w), bottomLeft: Radius.circular(15.w), bottomRight: Radius.circular(15.w), ), ), padding: EdgeInsets.symmetric( horizontal: 10.w, vertical: 4.w, ), margin: EdgeInsetsDirectional.only( start: 54.w, ).copyWith(bottom: 8.w), child: ExtendedText( widget.msg?.msg ?? '', style: TextStyle( fontSize: sp(13), color: Color( _msgColor(widget.msg.type ?? ""), ), fontWeight: FontWeight.w500, ), softWrap: true, // 自动换行 specialTextSpanBuilder: AtTextSpanBuilder( onTapCall: (userId) {}, ), ), ), onLongPress: () { if (!(widget.msg?.msg ?? "").startsWith(AtText.flag)) { Clipboard.setData( ClipboardData(text: widget.msg?.msg ?? ''), ); ATTts.show("Copied"); } }, ), ), ], ), ], ), ), ); } else { return Container( alignment: AlignmentDirectional.topStart, child: Container( padding: EdgeInsets.symmetric( horizontal: width(8.w), vertical: width(4), ), margin: EdgeInsets.symmetric( horizontal: width(10), ).copyWith(bottom: 6.w), decoration: BoxDecoration( color: Color(0xffB6A1DE).withOpacity(0.2), borderRadius: BorderRadius.all(Radius.circular(15.w)), ), child: Text( widget.msg?.msg ?? '', style: TextStyle( fontSize: sp(13), color: Color(_msgColor(widget.msg.type ?? "")), fontWeight: FontWeight.w500, ), textAlign: TextAlign.left, ), ), ); } } ///系统提示 _systemTipsRoomItem1(BuildContext context) { return Container( alignment: AlignmentDirectional.topStart, child: Container( padding: EdgeInsets.symmetric( horizontal: width(8.w), vertical: width(8), ), margin: EdgeInsets.symmetric( horizontal: width(10), ).copyWith(bottom: 8.w), decoration: BoxDecoration( color: Color(0xffB6A1DE).withOpacity(0.2), borderRadius: BorderRadius.all(Radius.circular(15.w)), ), child: Text( widget.msg?.msg ?? '', style: TextStyle( fontSize: sp(13), height: 1.5.w, color: Color(_msgColor(widget.msg.type ?? "")), fontWeight: FontWeight.w500, ), textAlign: TextAlign.left, ), ), ); } ///火箭用户中奖 _buildRocketRewardMsg1(BuildContext context) { List spans = []; spans.add( TextSpan( text: "${widget.msg.user?.userNickname} ", style: TextStyle( fontSize: sp(12), color: themeColor, fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ); spans.add( TextSpan( text: ATAppLocalizations.of(context)!.inRocket, style: TextStyle( fontSize: sp(12), color: Colors.white, fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ); spans.add( WidgetSpan( alignment: PlaceholderAlignment.middle, child: Image.asset( "images/room/at_icon_room_rocket_lv${widget.msg.number}.png", width: 18.w, ), ), ); spans.add( TextSpan( text: ATAppLocalizations.of(context)!.obtain, style: TextStyle( fontSize: sp(12), color: Colors.white, fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ); spans.add( WidgetSpan( alignment: PlaceholderAlignment.middle, child: widget.msg.role == ATActivityRewardATPropsType.GOLD.name ? Image.asset("atu_images/general/at_icon_jb.png", width: 18.w) : netImage(url: widget.msg.msg ?? "", width: 18.w), ), ); Widget text = Text.rich( TextSpan(children: spans), textAlign: TextAlign.left, strutStyle: StrutStyle( height: 1.8, // 行高倍数 fontWeight: FontWeight.w500, forceStrutHeight: true, // 强制应用行高 ), ); return Container( alignment: AlignmentDirectional.topStart, child: Container( padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w), margin: EdgeInsets.only(left: 34.w).copyWith(bottom: 8.w), decoration: BoxDecoration( color: Color(0xffB6A1DE).withOpacity(0.2), borderRadius: BorderRadius.all(Radius.circular(15.w)), ), child: text, ), ); } ///幸运礼物 _buildGameLuckyGiftMsg1(BuildContext context) { List spans = []; spans.add( TextSpan( text: "${widget.msg.user?.userNickname} ", style: TextStyle( fontSize: sp(12), color: themeColor, fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer() ..onTap = () { widget.onClick(widget.msg.user); }, ), ); spans.add( TextSpan( text: ATAppLocalizations.of(context)!.sendTo, style: TextStyle( fontSize: sp(12), color: Colors.white, fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ); spans.add( TextSpan( text: " ${widget.msg.toUser?.userNickname} ", style: TextStyle( fontSize: sp(12), color: themeColor, fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer() ..onTap = () { widget.onClick(widget.msg.toUser); }, ), ); ATGlobalConfig.lang == "ar" ? spans.add( WidgetSpan( alignment: PlaceholderAlignment.middle, child: Image.asset( "atu_images/general/at_icon_jb.png", width: 18.w, ), ), ) : spans.add( WidgetSpan( alignment: PlaceholderAlignment.middle, child: netImage(url: widget.msg.gift?.giftPhoto ?? "", width: 18.w), ), ); spans.add( TextSpan( text: " ${ATAppLocalizations.of(context)!.obtain}", style: TextStyle( fontSize: sp(12), color: themeColor, fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ); spans.add( TextSpan( text: " ${widget.msg.awardAmount}", style: TextStyle( fontSize: sp(12), color: themeColor, fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ); ATGlobalConfig.lang == "ar" ? spans.add( WidgetSpan( alignment: PlaceholderAlignment.middle, child: netImage(url: widget.msg.gift?.giftPhoto ?? "", width: 18.w), ), ) : spans.add( WidgetSpan( alignment: PlaceholderAlignment.middle, child: Image.asset( "atu_images/general/at_icon_jb.png", width: 18.w, ), ), ); Widget text = Text.rich( TextSpan(children: spans), textAlign: TextAlign.left, strutStyle: StrutStyle( height: 1.3, // 行高倍数 fontWeight: FontWeight.w500, forceStrutHeight: true, // 强制应用行高 ), ); return Container( alignment: AlignmentDirectional.topStart, child: Container( padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w), margin: EdgeInsets.only(left: 34.w).copyWith(bottom: 8.w), decoration: BoxDecoration( color: Color(0xffB6A1DE).withOpacity(0.2), borderRadius: BorderRadius.all(Radius.circular(15.w)), ), child: text, ), ); } ///幸运礼物中奖5倍以及以上 _buildGameLuckyGiftMsg_5(BuildContext context) { List spans = []; spans.add( TextSpan( text: "${widget.msg.user?.userNickname} ", style: TextStyle( fontSize: sp(12), color: Color(0xffFEF129), fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer() ..onTap = () { widget.onClick(widget.msg.user); }, ), ); spans.add( TextSpan( text: ATAppLocalizations.of(context)!.get, style: TextStyle( fontSize: sp(12), color: Colors.white, fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ); spans.add( TextSpan( text: " ${widget.msg.awardAmount} ", style: TextStyle( fontSize: sp(12), color: Color(0xffFEF129), fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ); spans.add( TextSpan( text: ATAppLocalizations.of(context)!.coins3, style: TextStyle( fontSize: sp(12), color: Color(0xffFEF129), fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ); spans.add( TextSpan( text: " ${ATAppLocalizations.of(context)!.receivedFromALuckyGift}", style: TextStyle( fontSize: sp(12), color: Colors.white, fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ); Widget text = Text.rich( TextSpan(children: spans), textAlign: TextAlign.left, strutStyle: StrutStyle( height: 1.2, // 行高倍数 fontWeight: FontWeight.w500, forceStrutHeight: true, // 强制应用行高 ), ); return Container( alignment: AlignmentDirectional.topStart, child: Container( constraints: BoxConstraints(minHeight: 68.w), padding: EdgeInsets.symmetric(vertical: 5.w), margin: EdgeInsets.only(left: 34.w).copyWith(bottom: 8.w), decoration: BoxDecoration( image: DecorationImage( image: AssetImage("atu_images/room/at_icon_luck_gift_msg_n_bg.png"), fit: BoxFit.fill, ), ), child: Row( children: [ SizedBox(width: 10.w), Expanded(child: text), Container( decoration: BoxDecoration( image: DecorationImage( image: AssetImage( "atu_images/room/at_icon_luck_gift_msg_n_ball.png", ), fit: BoxFit.fill, ), ), alignment: AlignmentDirectional.center, width: 55.w, height: 55.w, child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 10.w), buildNumForGame(widget.msg.msg ?? "0", size: 18.w), SizedBox(height: 3.w), ATGlobalConfig.lang == "ar" ? Image.asset( "atu_images/room/at_icon_times_text_ar.png", height: 12.w, ) : Image.asset( "atu_images/room/at_icon_times_text_en.png", height: 10.w, ), ], ), ), SizedBox(width: 15.w), ], ), ), ); } Widget _buildGiftMsg1(BuildContext context) { PropsResources? chatBox = widget.msg.user?.getChatBox(); return Container( alignment: AlignmentDirectional.topStart, padding: EdgeInsets.symmetric(horizontal: width(5.w), vertical: width(4)), margin: EdgeInsets.symmetric(horizontal: width(10)).copyWith(bottom: 8.w), child: Column( children: [ _buildUserInfoLine(context), SizedBox(height: 6.w), Row( children: [ Flexible( // 使用Flexible替代Expanded child: chatBox != null ? Container( constraints: BoxConstraints( maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 ), padding: EdgeInsets.symmetric(horizontal: 10.w), margin: EdgeInsetsDirectional.only( start: 34.w, ).copyWith(bottom: 1.w), child: NinePatchImage( scale: 3, alignment: Alignment.center, imageProvider: _getChatBoxImageProvider( chatBox.expand, ), sliceCachedKey: _buildNinePatchCacheKey( chatBox.expand, ), child: Text.rich( TextSpan( children: [ TextSpan( text: ATAppLocalizations.of(context)!.sendTo, style: TextStyle( fontSize: 13.sp, color: Colors.white, ), ), TextSpan(text: " "), TextSpan( text: widget.msg.toUser?.userNickname ?? "", style: TextStyle( fontSize: 13.sp, color: themeColor, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), TextSpan(text: " "), WidgetSpan( child: netImage( url: widget.msg.gift?.giftPhoto ?? "", width: 22.w, height: 22.w, ), ), TextSpan(text: " "), TextSpan( text: "x${widget.msg.number}", style: TextStyle( fontSize: 13.sp, color: Colors.white, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ], ), textAlign: TextAlign.left, ), ), ) : Container( constraints: BoxConstraints( maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 ), decoration: BoxDecoration( color: Color(0xffB6A1DE).withOpacity(0.2), borderRadius: BorderRadius.only( topRight: Radius.circular(15.w), bottomLeft: Radius.circular(15.w), bottomRight: Radius.circular(15.w), ), ), padding: EdgeInsets.symmetric( horizontal: 10.w, vertical: 4.w, ), margin: EdgeInsetsDirectional.only( start: 54.w, ).copyWith(bottom: 8.w), child: Text.rich( TextSpan( children: [ TextSpan( text: ATAppLocalizations.of(context)!.sendTo, style: TextStyle( fontSize: 13.sp, color: Colors.white, ), ), TextSpan(text: " "), TextSpan( text: widget.msg.toUser?.userNickname ?? "", style: TextStyle( fontSize: 13.sp, color: themeColor, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), TextSpan(text: " "), WidgetSpan( child: netImage( url: widget.msg.gift?.giftPhoto ?? "", width: 22.w, height: 22.w, ), ), TextSpan(text: " "), TextSpan( text: "x${widget.msg.number}", style: TextStyle( fontSize: 13.sp, color: Colors.white, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ], ), textAlign: TextAlign.left, ), ), ), ], ), ], ), ); } ///进入房间 _enterRoomItem1(BuildContext context) { List spans = []; if (widget.msg.role != null && widget.msg.role != 0) { spans.add( WidgetSpan( alignment: PlaceholderAlignment.middle, child: Padding( padding: EdgeInsets.only(right: 3.w), child: msgRoleTag(widget.msg.role ?? "", width: 18.w), ), ), ); } spans.add( TextSpan( text: "${widget.msg?.user?.userNickname ?? ''} ", style: TextStyle( fontSize: sp(13), color: _roleNameColor(widget.msg.role ?? ""), fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ); spans.add( TextSpan( text: ATAppLocalizations.of(context)!.joinRoomTips, style: TextStyle( fontSize: sp(13), color: Color(_msgColor(widget.msg.type ?? "")), fontWeight: FontWeight.w500, ), ), ); Widget text = Text.rich( TextSpan(children: spans), textAlign: TextAlign.left, ); return Container( alignment: AlignmentDirectional.topStart, child: Container( padding: EdgeInsets.symmetric( horizontal: width(8.w), vertical: width(4), ), margin: EdgeInsets.symmetric( horizontal: width(10), ).copyWith(bottom: 8.w), child: text, ), ); } Widget _buildMedals(List wearBadge) { return wearBadge.isNotEmpty ? Expanded( child: SingleChildScrollView( child: SizedBox( height: 25.w, child: ListView.separated( scrollDirection: Axis.horizontal, shrinkWrap: true, itemCount: wearBadge.length, itemBuilder: (context, index) { return netImage( width: 25.w, height: 25.w, url: wearBadge[index].selectUrl ?? "", ); }, separatorBuilder: (BuildContext context, int index) { return SizedBox(width: 5.w); }, ), ), ), ) : Container(); } String _buildNinePatchCacheKey(String? url) { final String normalizedUrl = (url ?? "").trim(); if (normalizedUrl.isEmpty) { return ""; } // Reuse the parsed 9-patch slices for the same chat box background so // rapid room message updates don't repeatedly rebuild bubble geometry. return "$normalizedUrl|scale:3|v3"; } int _msgColor(String type) { switch (type) { case ATRoomMsgType.joinRoom: case ATRoomMsgType.systemTips: case ATRoomMsgType.welcome: return 0xFFFFFFFF; case ATRoomMsgType.gift: return 0xFFFFFFFF; case ATRoomMsgType.shangMai: case ATRoomMsgType.xiaMai: case ATRoomMsgType.qcfj: case ATRoomMsgType.killXiaMai: return 0xFFFFFFFF; break; // case type.text: // return _mapTextColor(); // break; default: return 0xFFFFFFFF; } } _roleNameColor(String role) { if (ATRoomRolesType.HOMEOWNER.name == role) { return Color(0xff2AC0F2); } else if (ATRoomRolesType.ADMIN.name == role) { return Color(0xffF9A024); } else if (ATRoomRolesType.MEMBER.name == role) { return Color(0xff28ED77); } else { return Colors.white; } } Widget _buildRoleChangeMsg1(BuildContext context) { var text = ""; if (ATRoomRolesType.ADMIN.name == widget.msg.msg) { text = ATAppLocalizations.of(context)!.adminByHomeowner; } else if (ATRoomRolesType.MEMBER.name == widget.msg.msg) { text = ATAppLocalizations.of(context)!.memberByHomeowner; } else { text = ATAppLocalizations.of(context)!.touristByHomeowner; } List spans = []; if (widget.msg.toUser != null) { spans.add( TextSpan( text: "${widget.msg.toUser?.userNickname ?? ''} ", style: TextStyle( fontSize: sp(13), color: Color(0xff2AC0F2), fontWeight: FontWeight.w500, ), recognizer: TapGestureRecognizer()..onTap = () {}, ), ); } spans.add( TextSpan( text: text, style: TextStyle( fontSize: sp(13), color: Colors.white, fontWeight: FontWeight.w500, ), ), ); Widget textView = Text.rich( TextSpan(children: spans), textAlign: TextAlign.left, ); return Container( alignment: AlignmentDirectional.topStart, child: Container( padding: EdgeInsets.symmetric( horizontal: width(8.w), vertical: width(8), ), margin: EdgeInsets.symmetric( horizontal: width(10), ).copyWith(bottom: 8.w), decoration: BoxDecoration( color: Color(0xffB6A1DE).withOpacity(0.2), borderRadius: BorderRadius.all(Radius.circular(15.w)), ), child: textView, ), ); } Widget _buildImageMsg1(BuildContext context) { PropsResources? chatBox = widget.msg.user?.getChatBox(); return Container( alignment: AlignmentDirectional.topStart, child: Container( padding: EdgeInsets.symmetric( horizontal: width(5.w), vertical: width(4), ), margin: EdgeInsets.symmetric( horizontal: width(10), ).copyWith(bottom: 6.w), child: Column( children: [ _buildUserInfoLine(context), SizedBox(height: 6.w), Row( children: [ Flexible( // 使用Flexible替代Expanded child: GestureDetector( child: chatBox != null ? Container( constraints: BoxConstraints( maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束 ), padding: EdgeInsets.symmetric(horizontal: 10.w), margin: EdgeInsetsDirectional.only( start: 34.w, ).copyWith(bottom: 1.w), child: NinePatchImage( scale: 3, alignment: Alignment.center, imageProvider: _getChatBoxImageProvider( chatBox.expand, ), sliceCachedKey: _buildNinePatchCacheKey( chatBox.expand, ), child: netImage(url: widget.msg.msg ?? ""), ), ) : Container( constraints: BoxConstraints( maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束 ), decoration: BoxDecoration( color: Color(0xffB6A1DE).withOpacity(0.2), borderRadius: BorderRadius.only( topRight: Radius.circular(15.w), bottomLeft: Radius.circular(15.w), bottomRight: Radius.circular(15.w), ), ), padding: EdgeInsets.symmetric( horizontal: 10.w, vertical: 4.w, ), margin: EdgeInsetsDirectional.only( start: 54.w, ).copyWith(bottom: 8.w), child: netImage(url: widget.msg.msg ?? ""), ), onTap: () { String encodedUrls = Uri.encodeComponent( jsonEncode([widget.msg.msg]), ); ATNavigatorUtils.push( context, "${MainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", ); }, ), ), ], ), ], ), ), ); } Widget _buildDiceMsg1(BuildContext context) { PropsResources? chatBox = widget.msg.user?.getChatBox(); return Container( alignment: AlignmentDirectional.topStart, child: Container( padding: EdgeInsets.symmetric( horizontal: width(5.w), vertical: width(4), ), margin: EdgeInsets.symmetric( horizontal: width(10), ).copyWith(bottom: 6.w), child: Column( children: [ _buildUserInfoLine(context), SizedBox(height: 6.w), Row( children: [ Flexible( // 使用Flexible替代Expanded child: GestureDetector( child: chatBox != null ? Container( constraints: BoxConstraints( maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束 ), padding: EdgeInsets.symmetric(horizontal: 10.w), margin: EdgeInsetsDirectional.only( start: 34.w, ).copyWith(bottom: 1.w), child: NinePatchImage( scale: 3, alignment: Alignment.center, imageProvider: _getChatBoxImageProvider( chatBox.expand, ), sliceCachedKey: _buildNinePatchCacheKey( chatBox.expand, ), child: (widget.msg.hasPlayedAnimation ?? false) ? Image.asset( "atu_images/room/at_icon_dice_${widget.msg.msg}.png", height: 35.w, ) : FutureBuilder( future: Future.delayed( Duration(milliseconds: 2000), ), // 传入Future builder: ( BuildContext context, AsyncSnapshot snapshot, ) { // 根据snapshot的状态来构建UI if (snapshot.connectionState == ConnectionState.done) { widget.msg.hasPlayedAnimation = true; return Image.asset( "atu_images/room/at_icon_dice_${widget.msg.msg}.png", height: 35.w, ); } else { return Image.asset( "atu_images/room/at_icon_dice_animl.webp", height: 35.w, ); } }, ), ), ) : Container( constraints: BoxConstraints( maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束 ), decoration: BoxDecoration( color: Color(0xffB6A1DE).withOpacity(0.2), borderRadius: BorderRadius.only( topRight: Radius.circular(15.w), bottomLeft: Radius.circular(15.w), bottomRight: Radius.circular(15.w), ), ), padding: EdgeInsets.symmetric( horizontal: 10.w, vertical: 4.w, ), margin: EdgeInsetsDirectional.only( start: 54.w, ).copyWith(bottom: 8.w), child: (widget.msg.hasPlayedAnimation ?? false) ? Image.asset( "atu_images/room/at_icon_dice_${widget.msg.msg}.png", height: 35.w, ) : FutureBuilder( future: Future.delayed( Duration(milliseconds: 2000), ), // 传入Future builder: ( BuildContext context, AsyncSnapshot snapshot, ) { // 根据snapshot的状态来构建UI if (snapshot.connectionState == ConnectionState.done) { widget.msg.hasPlayedAnimation = true; return Image.asset( "atu_images/room/at_icon_dice_${widget.msg.msg}.png", height: 35.w, ); } else { return Image.asset( "atu_images/room/at_icon_dice_animl.webp", height: 35.w, ); } }, ), ), onTap: () {}, ), ), ], ), ], ), ), ); } Widget _buildRPSMsg1(BuildContext context) { PropsResources? chatBox = widget.msg.user?.getChatBox(); return Container( alignment: AlignmentDirectional.topStart, child: Container( padding: EdgeInsets.symmetric( horizontal: width(5.w), vertical: width(4), ), margin: EdgeInsets.symmetric( horizontal: width(10), ).copyWith(bottom: 6.w), child: Column( children: [ _buildUserInfoLine(context), SizedBox(height: 6.w), Row( children: [ Flexible( // 使用Flexible替代Expanded child: GestureDetector( child: chatBox != null ? Container( constraints: BoxConstraints( maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束 ), padding: EdgeInsets.symmetric(horizontal: 10.w), margin: EdgeInsetsDirectional.only( start: 34.w, ).copyWith(bottom: 1.w), child: NinePatchImage( scale: 3, alignment: Alignment.center, imageProvider: _getChatBoxImageProvider( chatBox.expand, ), sliceCachedKey: _buildNinePatchCacheKey( chatBox.expand, ), child: (widget.msg.hasPlayedAnimation ?? false) ? Image.asset( "atu_images/room/at_icon_rps_${widget.msg.msg}.png", height: 35.w, ) : FutureBuilder( future: Future.delayed( Duration(milliseconds: 2000), ), // 传入Future builder: ( BuildContext context, AsyncSnapshot snapshot, ) { // 根据snapshot的状态来构建UI if (snapshot.connectionState == ConnectionState.done) { widget.msg.hasPlayedAnimation = true; return Image.asset( "atu_images/room/at_icon_rps_${widget.msg.msg}.png", height: 35.w, ); } else { return Image.asset( "atu_images/room/at_icon_rps_animal.webp", height: 35.w, ); } }, ), ), ) : Container( constraints: BoxConstraints( maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束 ), decoration: BoxDecoration( color: Color(0xffB6A1DE).withOpacity(0.2), borderRadius: BorderRadius.only( topRight: Radius.circular(15.w), bottomLeft: Radius.circular(15.w), bottomRight: Radius.circular(15.w), ), ), padding: EdgeInsets.symmetric( horizontal: 10.w, vertical: 4.w, ), margin: EdgeInsetsDirectional.only( start: 54.w, ).copyWith(bottom: 8.w), child: (widget.msg.hasPlayedAnimation ?? false) ? Image.asset( "atu_images/room/at_icon_rps_${widget.msg.msg}.png", height: 35.w, ) : FutureBuilder( future: Future.delayed( Duration(milliseconds: 2000), ), // 传入Future builder: ( BuildContext context, AsyncSnapshot snapshot, ) { // 根据snapshot的状态来构建UI if (snapshot.connectionState == ConnectionState.done) { widget.msg.hasPlayedAnimation = true; return Image.asset( "atu_images/room/at_icon_rps_${widget.msg.msg}.png", height: 35.w, ); } else { return Image.asset( "atu_images/room/at_icon_rps_animal.webp", height: 35.w, ); } }, ), ), onTap: () {}, ), ), ], ), ], ), ), ); } Widget _buildLuckNumberMsg1(BuildContext context) { PropsResources? chatBox = widget.msg.user?.getChatBox(); return Container( alignment: AlignmentDirectional.topStart, child: Container( padding: EdgeInsets.symmetric( horizontal: width(5.w), vertical: width(4), ), margin: EdgeInsets.symmetric( horizontal: width(10), ).copyWith(bottom: 6.w), child: Column( children: [ _buildUserInfoLine(context), SizedBox(height: 6.w), Row( children: [ Flexible( // 使用Flexible替代Expanded child: GestureDetector( child: chatBox != null ? Container( constraints: BoxConstraints( maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 ), padding: EdgeInsets.symmetric(horizontal: 10.w), margin: EdgeInsetsDirectional.only( start: 34.w, ).copyWith(bottom: 1.w), child: NinePatchImage( scale: 3, alignment: Alignment.center, imageProvider: _getChatBoxImageProvider( chatBox.expand, ), sliceCachedKey: _buildNinePatchCacheKey( chatBox.expand, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ ATGlobalConfig.lang == "ar" ? Image.asset( "atu_images/room/at_icon_luck_num_text_ar.png", height: 20.w, ) : Image.asset( "atu_images/room/at_icon_luck_num_text_en.png", height: 20.w, ), buildNumForRoomLuckNum( widget.msg.msg ?? "", size: 20.w, ), ], ), ), ) : Container( constraints: BoxConstraints( maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束 ), decoration: BoxDecoration( color: Color(0xffB6A1DE).withOpacity(0.2), borderRadius: BorderRadius.only( topRight: Radius.circular(15.w), bottomLeft: Radius.circular(15.w), bottomRight: Radius.circular(15.w), ), ), padding: EdgeInsets.symmetric( horizontal: 10.w, vertical: 4.w, ), margin: EdgeInsetsDirectional.only( start: 54.w, ).copyWith(bottom: 8.w), child: Row( mainAxisSize: MainAxisSize.min, children: [ ATGlobalConfig.lang == "ar" ? Image.asset( "atu_images/room/at_icon_luck_num_text_ar.png", height: 20.w, ) : Image.asset( "atu_images/room/at_icon_luck_num_text_en.png", height: 20.w, ), buildNumForRoomLuckNum( widget.msg.msg ?? "", size: 20.w, ), ], ), ), onTap: () {}, ), ), ], ), ], ), ), ); } _buildUserInfoLine(BuildContext context) { final String userId = widget.msg.user?.id ?? ""; if (userId.isEmpty) { return const SizedBox.shrink(); } final bool shouldRefreshUserInfo = widget.msg.needUpDataUserInfo ?? false; if (shouldRefreshUserInfo) { ATRoomUtils.roomUsersMap.remove(userId); MsgItem.roomUserFutureCache.remove(userId); MsgItem.roomUserFullInfoLoaded.remove(userId); MsgItem.roomUserRefreshInFlight.add(userId); widget.msg.needUpDataUserInfo = false; } final ChatVibeUserProfile? msgUser = widget.msg.user; ChatVibeUserProfile? cachedUser = ATRoomUtils.roomUsersMap[userId]; if (cachedUser != null && MsgItem.roomUserFullInfoLoaded.contains(userId)) { return _buildNewUserInfoLine(context, cachedUser); } final ChatVibeUserProfile? fallbackUser = cachedUser ?? msgUser; return FutureBuilder( future: _getOrLoadRoomUserInfo(userId), builder: (ct, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasData) { ChatVibeUserProfile user = snapshot.data!; ATRoomUtils.roomUsersMap[user.id ?? ""] = user; MsgItem.roomUserFullInfoLoaded.add(userId); MsgItem.roomUserRefreshInFlight.remove(userId); return _buildNewUserInfoLine(context, user); } if (snapshot.hasError) { MsgItem.roomUserFutureCache.remove(userId); MsgItem.roomUserRefreshInFlight.remove(userId); } } if (fallbackUser != null) { return _buildNewUserInfoLine(context, fallbackUser); } final PropsResources? activeVip = _getActiveVip(widget.msg.user); return Row( children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { widget.onClick(widget.msg.user); }, child: Stack( alignment: AlignmentDirectional.bottomEnd, children: [ head( url: widget.msg.user?.userAvatar ?? "", width: 48.w, // headdress: msg.user?.getHeaddress()?.sourceUrl, ), Positioned( child: msgRoleTag(widget.msg.role ?? "", width: 14.w), bottom: 2.w, right: 2.w, ), ], ), ), SizedBox(width: 3.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ getWealthLevel( widget.msg.user?.wealthLevel ?? 0, width: 48.w, height: 23.w, fontSize: 10.sp, ), SizedBox(width: 3.w), getUserLevel( widget.msg.user?.charmLevel ?? 0, width: 48.w, height: 23.w, fontSize: 10.sp, ), SizedBox(width: 3.w), chatvibeNickNameText( maxWidth: 120.w, fontWeight: FontWeight.w500, widget.msg.user?.userNickname ?? "", fontSize: 13.sp, type: activeVip?.name ?? "", needScroll: (widget.msg.user?.userNickname?.characters.length ?? 0) > 10, ), ], ), Row( mainAxisSize: MainAxisSize.min, children: [ activeVip != null ? _buildStableBadgeImage( url: activeVip.cover ?? "", width: 25.w, height: 25.w, ) : Container(), _buildMedals( (widget.msg.user?.wearBadge?.where((item) { return item.use ?? false; }).toList() ?? []), ), ], ), ], ), ), ], ); }, ); } PropsResources? _getActiveVip(ChatVibeUserProfile? user) { final List? useProps = user?.useProps; if (useProps == null || useProps.isEmpty) { return null; } final int now = DateTime.now().millisecondsSinceEpoch; PropsResources? activeVip; PropsResources? fallbackVipWithoutExpire; int latestExpireTime = 0; for (final UseProps useProp in useProps) { if (useProp.propsResources?.type != ATPropsType.NOBLE_VIP.name) { continue; } final int? parsedExpire = int.tryParse(useProp.expireTime ?? ""); if (parsedExpire == null || parsedExpire <= 0) { fallbackVipWithoutExpire ??= useProp.propsResources; continue; } final int expireTime = parsedExpire; if (expireTime > now && expireTime >= latestExpireTime) { latestExpireTime = expireTime; activeVip = useProp.propsResources; } } return activeVip ?? fallbackVipWithoutExpire; } Widget _buildStableBadgeImage({ required String? url, required double width, required double height, }) { final String normalizedUrl = (url ?? "").trim(); if (normalizedUrl.isEmpty) { return const SizedBox.shrink(); } return RepaintBoundary( child: Image( image: _getBadgeImageProvider(normalizedUrl), width: width, height: height, fit: BoxFit.contain, gaplessPlayback: true, filterQuality: FilterQuality.low, errorBuilder: (context, error, stackTrace) { return const SizedBox.shrink(); }, ), ); } ExtendedNetworkImageProvider _getChatBoxImageProvider(String? url) { final String normalizedUrl = (url ?? "").trim(); final ExtendedNetworkImageProvider? cached = MsgItem.chatBoxImageProviderCache[normalizedUrl]; if (cached != null) { return cached; } if (MsgItem.chatBoxImageProviderCache.length >= _maxChatBoxImageProviderCacheSize) { MsgItem.chatBoxImageProviderCache.remove( MsgItem.chatBoxImageProviderCache.keys.first, ); } final ExtendedNetworkImageProvider provider = ExtendedNetworkImageProvider( normalizedUrl, cache: true, cacheMaxAge: _chatBoxImageCacheMaxAge, ); MsgItem.chatBoxImageProviderCache[normalizedUrl] = provider; return provider; } ExtendedNetworkImageProvider _getBadgeImageProvider(String? url) { final String normalizedUrl = (url ?? "").trim(); final ExtendedNetworkImageProvider? cached = MsgItem.badgeImageProviderCache[normalizedUrl]; if (cached != null) { return cached; } if (MsgItem.badgeImageProviderCache.length >= _maxBadgeImageProviderCacheSize) { MsgItem.badgeImageProviderCache.remove( MsgItem.badgeImageProviderCache.keys.first, ); } final ExtendedNetworkImageProvider provider = ExtendedNetworkImageProvider( normalizedUrl, cache: true, cacheMaxAge: _badgeImageCacheMaxAge, ); MsgItem.badgeImageProviderCache[normalizedUrl] = provider; return provider; } Future _getOrLoadRoomUserInfo(String userId) { final Future? cached = MsgItem.roomUserFutureCache[userId]; if (cached != null) { return cached; } if (MsgItem.roomUserFutureCache.length >= _maxRoomUserFutureCacheSize) { MsgItem.roomUserFutureCache.remove( MsgItem.roomUserFutureCache.keys.first, ); } final Future future = AccountRepository().loadUserInfo( userId, ); MsgItem.roomUserFutureCache[userId] = future; return future; } _buildNewUserInfoLine(BuildContext context, ChatVibeUserProfile user) { final PropsResources? activeVip = _getActiveVip(user); return Row( children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { widget.onClick(user); }, child: Stack( alignment: AlignmentDirectional.bottomEnd, children: [ head( url: user.userAvatar ?? "", width: 48.w, // headdress: msg.user?.getHeaddress()?.sourceUrl, ), Positioned( child: msgRoleTag(widget.msg.role ?? "", width: 14.w), bottom: 2.w, right: 2.w, ), ], ), ), SizedBox(width: 3.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ getWealthLevel( user.wealthLevel ?? 0, width: 48.w, height: 23.w, fontSize: 10.sp, ), SizedBox(width: 3.w), getUserLevel( user.charmLevel ?? 0, width: 48.w, height: 23.w, fontSize: 10.sp, ), SizedBox(width: 3.w), chatvibeNickNameText( maxWidth: 120.w, fontWeight: FontWeight.w500, user.userNickname ?? "", fontSize: 13.sp, type: activeVip?.name ?? "", needScroll: (user.userNickname?.characters.length ?? 0) > 10, ), ], ), Row( mainAxisSize: MainAxisSize.min, children: [ activeVip != null ? _buildStableBadgeImage( url: activeVip.cover ?? "", width: 25.w, height: 25.w, ) : Container(), _buildMedals( user.wearBadge?.where((item) { return item.use ?? false; }).toList() ?? [], ), ], ), ], ), ), ], ); } } ///消息 class Msg { String? groupId; String? msg; MicRes? userWheat; String? type; ChatVibeUserProfile? user; String? role = ""; ChatVibeGiftRes? gift; //礼物个数 num? number; num? awardAmount; // 送礼给谁 ChatVibeUserProfile? toUser; int? time = 0; //动画是否已经播放(骰子、石头剪刀布、随机数字) bool? hasPlayedAnimation = false; bool? needUpDataUserInfo = false; Msg({ required this.groupId, required this.msg, required this.type, this.user, this.toUser, this.userWheat, this.role = "", this.gift, this.number, this.awardAmount, this.hasPlayedAnimation, this.needUpDataUserInfo, }) { time = DateTime.now().millisecondsSinceEpoch; } Msg.fromJson(dynamic json) { groupId = json['groupId']; msg = json['msg']; type = json['type']; role = json['role']; time = json['time']; number = json['number']; awardAmount = json['awardAmount']; hasPlayedAnimation = json['hasPlayedAnimation']; needUpDataUserInfo = json['needUpDataUserInfo']; userWheat = json['userWheat'] != null ? MicRes.fromJson(json['userWheat']) : null; user = json['user'] != null ? ChatVibeUserProfile.fromJson(json['user']) : null; toUser = json['toUser'] != null ? ChatVibeUserProfile.fromJson(json['toUser']) : null; gift = json['gift'] != null ? ChatVibeGiftRes.fromJson(json['gift']) : null; } Map toJson() { final map = {}; map['groupId'] = groupId; map['msg'] = msg; map['type'] = type; map['role'] = role; map['time'] = time; map['awardAmount'] = awardAmount; map['number'] = number; map['hasPlayedAnimation'] = hasPlayedAnimation; map['needUpDataUserInfo'] = needUpDataUserInfo; if (userWheat != null) { map['userWheat'] = userWheat?.toJson(); } if (user != null) { map['user'] = user?.toJson(); } if (toUser != null) { map['toUser'] = toUser?.toJson(); } if (gift != null) { map['gift'] = gift?.toJson(); } return map; } }