yumi-flutter/lib/ui_kit/widgets/room/room_msg_item.dart
2026-06-02 19:57:41 +08:00

2228 lines
68 KiB
Dart

import 'dart:convert';
import 'dart:math' as math;
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_screenutil/flutter_screenutil.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/tools/sc_room_utils.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
import 'package:yumi/app/constants/sc_room_msg_type.dart';
import 'package:yumi/app/constants/sc_screen.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
import 'package:yumi/modules/index/main_route.dart';
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart';
import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart';
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_chat_message.dart';
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
import '../../../shared/data_sources/models/enum/sc_activity_reward_props_type.dart';
import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart';
import '../../components/sc_compontent.dart';
import '../../components/sc_tts.dart';
import '../../components/text/sc_text.dart';
const EdgeInsets _vipBubbleCenterSliceRatio = EdgeInsets.fromLTRB(
0.34,
0.34,
0.34,
0.34,
);
const EdgeInsets _vipBubbleSourceEdgeInset = EdgeInsets.all(2);
const String _cpSystemNoticeBubbleAsset =
'sc_images/room/cp_invite/sc_cp_system_notice_bubble.png';
const Rect _cpSystemNoticeBubbleCenterSlice = Rect.fromLTRB(42, 6, 209, 43);
///消息item
class MsgItem extends StatefulWidget {
final Function(SocialChatUserProfile? user) onClick;
final Msg msg;
const MsgItem({super.key, required this.msg, required this.onClick});
@override
State<MsgItem> createState() => _MsgItemState();
}
class _MsgItemState extends State<MsgItem> {
static const Duration _userInfoFailureRetryInterval = Duration(minutes: 5);
static final Map<String, Future<SocialChatUserProfile?>> _userInfoFutures =
{};
static final Map<String, DateTime> _userInfoFailedAt = {};
@override
Widget build(BuildContext context) {
//gift
// if (msg.type == Roomtype.gift.code) {
// return _buildGiftMsg(context);
// }
///系统提示
if (widget.msg.type == SCRoomMsgType.systemTips ||
widget.msg.type == SCRoomMsgType.cpSystemNotice) {
return _systemTipsRoomItem(context);
}
///进入房间
if (widget.msg.type == SCRoomMsgType.joinRoom) {
return _enterRoomItem(context);
}
///礼物
if (widget.msg.type == SCRoomMsgType.gift ||
widget.msg.type == SCRoomMsgType.luckGiftAnimOther) {
return _buildGiftMsg(context);
}
///火箭用户中奖
if (SCRoomMsgType.isRocketRewardUserType(widget.msg.type)) {
return _buildRocketRewardMsg(context);
}
///火箭燃料加满倒计时
if (widget.msg.type == SCRoomMsgType.rocketLaunchNotice) {
return _buildRocketLaunchNoticeMsg(context);
}
///幸运礼物
if (widget.msg.type == SCRoomMsgType.gameLuckyGift) {
return _buildGameLuckyGiftMsg(context);
}
///幸运礼物
if (widget.msg.type == SCRoomMsgType.gameLuckyGift_5) {
return _buildGameLuckyGiftMsg_5(context);
}
///房间身份变动
if (widget.msg.type == SCRoomMsgType.roomRoleChange) {
return _buildRoleChangeMsg(context);
}
///踢出房间
if (widget.msg.type == SCRoomMsgType.qcfj) {
widget.msg.msg = SCAppLocalizations.of(context)!.kickRoomTips;
}
///图片消息
if (widget.msg.type == SCRoomMsgType.image) {
return _buildImageMsg(context);
}
///表情包聊天消息
if (widget.msg.type == SCRoomMsgType.emoticons) {
return _buildEmoticonMsg(context);
}
///掷骰子
if (widget.msg.type == SCRoomMsgType.roomDice) {
return _buildDiceMsg(context);
}
///石头剪刀布
if (widget.msg.type == SCRoomMsgType.roomRPS) {
return _buildRPSMsg(context);
}
///幸运数字
if (widget.msg.type == SCRoomMsgType.roomLuckNumber) {
return _buildLuckNumberMsg(context);
}
///房间红包
if (widget.msg.type == SCRoomMsgType.roomRedPacket) {
return _buildRoomRedPacketMsg(context);
}
if (widget.msg.type == SCRoomMsgType.roomRedPacketClaim) {
return _buildRoomRedPacketClaimMsg(context);
}
if (widget.msg.user != null) {
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: _buildTextMessageBubble(),
onLongPress: () {
if (!(widget.msg.msg ?? "").startsWith(AtText.flag)) {
Clipboard.setData(
ClipboardData(text: widget.msg.msg ?? ''),
);
SCTts.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: Colors.black38,
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,
),
),
);
}
}
Widget _buildTextMessageBubble() {
final chatBubble = widget.msg.user?.getChatBox();
final hasChatBubble = _resolveRoomChatBubbleImageUrl(chatBubble).isNotEmpty;
final bubbleStartMargin = 54.w;
return LayoutBuilder(
builder: (context, constraints) {
final rowMaxWidth =
constraints.maxWidth.isFinite
? constraints.maxWidth
: ScreenUtil().screenWidth;
final availableBubbleWidth = math.max(
1.0,
rowMaxWidth - bubbleStartMargin,
);
if (hasChatBubble) {
return _buildVipTextMessageBubble(
chatBubble,
maxBubbleWidth: availableBubbleWidth,
startMargin: bubbleStartMargin,
);
}
return Container(
constraints: BoxConstraints(
maxWidth: math.min(
ScreenUtil().screenWidth * 0.7,
availableBubbleWidth,
),
),
decoration: BoxDecoration(
color: Colors.black38,
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: bubbleStartMargin,
).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) {}),
),
);
},
);
}
Widget _buildRoomRedPacketMsg(BuildContext context) {
final data = RoomRedPacketUiData.fromMessagePayload(
payload: widget.msg.msg,
fallbackUser: widget.msg.user,
);
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.msg.user != null) ...[
_buildUserInfoLine(context),
SizedBox(height: 6.w),
],
RoomRedPacketChatMessage(data: data),
],
),
),
);
}
Widget _buildRoomRedPacketClaimMsg(BuildContext context) {
final localizations = SCAppLocalizations.of(context)!;
final packetSenderName =
(widget.msg.msg ?? '').trim().isNotEmpty
? (widget.msg.msg ?? '').trim()
: localizations.redEnvelope;
final message = localizations.roomRedPacketReceivedMessage(
_formatAwardAmount(widget.msg.number),
packetSenderName,
);
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.msg.user != null) ...[
_buildUserInfoLine(context),
SizedBox(height: 6.w),
],
Container(
constraints: BoxConstraints(
maxWidth: ScreenUtil().screenWidth * 0.7,
),
decoration: BoxDecoration(
color: Colors.black38,
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(
message,
style: TextStyle(
fontSize: sp(13),
color: Color(_msgColor(widget.msg.type ?? "")),
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.start,
),
),
],
),
),
);
}
Widget _buildVipTextMessageBubble(
PropsResources? resource, {
required double maxBubbleWidth,
required double startMargin,
}) {
final messageText = widget.msg.msg ?? "";
final textStyle = TextStyle(
fontSize: sp(13),
color: Color(_msgColor(widget.msg.type ?? "")),
fontWeight: FontWeight.w500,
);
final imageUrl = _resolveRoomChatBubbleImageUrl(resource);
final multilineMaxBubbleWidth = math.min(maxBubbleWidth, 300.w);
final multilineMaxTextWidth = math.max(
82.w,
multilineMaxBubbleWidth - 64.w,
);
final multilineTextPainter = TextPainter(
text: TextSpan(text: messageText, style: textStyle),
textDirection: Directionality.of(context),
)..layout(maxWidth: multilineMaxTextWidth);
if (multilineTextPainter.computeLineMetrics().length > 1) {
return _buildMultilineVipTextMessageBubble(
messageText: messageText,
textStyle: textStyle,
imageUrl: imageUrl,
textSize: multilineTextPainter.size,
maxBubbleWidth: multilineMaxBubbleWidth,
startMargin: startMargin,
);
}
return _buildSingleLineVipTextMessageBubble(
messageText: messageText,
textStyle: textStyle,
imageUrl: imageUrl,
maxBubbleWidth: maxBubbleWidth,
startMargin: startMargin,
);
}
Widget _buildMultilineVipTextMessageBubble({
required String messageText,
required TextStyle textStyle,
required String imageUrl,
required Size textSize,
required double maxBubbleWidth,
required double startMargin,
}) {
final leftPadding = 22.w;
final rightPadding = 42.w;
final topPadding = 16.w;
final bottomPadding = 24.w;
final bubbleWidth = math
.max(126.w, textSize.width + leftPadding + rightPadding)
.clamp(126.w, maxBubbleWidth);
final textMaxWidth = math.max(
64.w,
bubbleWidth - leftPadding - rightPadding,
);
final textPainter = TextPainter(
text: TextSpan(text: messageText, style: textStyle),
textDirection: Directionality.of(context),
)..layout(maxWidth: textMaxWidth);
final textBoxHeight = textPainter.size.height + 2.w;
final bubbleHeight = math.max(
76.w,
textBoxHeight + topPadding + bottomPadding,
);
return Container(
width: bubbleWidth,
height: bubbleHeight,
margin: EdgeInsetsDirectional.only(
start: startMargin,
).copyWith(bottom: 8.w),
child: Stack(
fit: StackFit.expand,
children: [
_buildVipBubbleBackground(
url: imageUrl,
width: bubbleWidth,
height: bubbleHeight,
fallback: SizedBox(width: bubbleWidth, height: bubbleHeight),
),
PositionedDirectional(
start: leftPadding,
top: topPadding,
width: textMaxWidth,
child: ExtendedText(
messageText,
style: textStyle,
softWrap: true,
specialTextSpanBuilder: AtTextSpanBuilder(onTapCall: (userId) {}),
),
),
],
),
);
}
Widget _buildSingleLineVipTextMessageBubble({
required String messageText,
required TextStyle textStyle,
required String imageUrl,
required double maxBubbleWidth,
required double startMargin,
}) {
final leftPadding = 22.w;
final rightPadding = 24.w;
final topPadding = 12.w;
final bottomPadding = 16.w;
final minSkinWidth = 96.w;
final minSkinHeight = 58.w;
final textWidthSafety = 10.w;
final maxTextWidth = math.max(
1.0,
maxBubbleWidth - leftPadding - rightPadding,
);
final textSize = _measureMessageTextSize(
text: messageText,
style: textStyle,
maxWidth: maxTextWidth,
);
final bubbleWidth = math.min(
maxBubbleWidth,
math.max(
minSkinWidth,
textSize.width + textWidthSafety + leftPadding + rightPadding,
),
);
final textMaxWidth = math.max(
1.0,
bubbleWidth - leftPadding - rightPadding,
);
final bubbleHeight = math.max(
minSkinHeight,
textSize.height + topPadding + bottomPadding,
);
return Container(
width: bubbleWidth,
height: bubbleHeight,
margin: EdgeInsetsDirectional.only(
start: startMargin,
).copyWith(bottom: 8.w),
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
Positioned.fill(
child: _buildVipBubbleSkin(
url: imageUrl,
width: math.max(bubbleWidth, minSkinWidth),
height: math.max(bubbleHeight, minSkinHeight),
alignment: AlignmentDirectional.centerStart,
),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(
leftPadding,
topPadding,
rightPadding,
bottomPadding,
),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: SizedBox(
width: textMaxWidth,
child: ExtendedText(
messageText,
style: textStyle,
maxLines: 1,
softWrap: false,
specialTextSpanBuilder: AtTextSpanBuilder(
onTapCall: (userId) {},
),
),
),
),
),
],
),
);
}
Widget _buildVipBubbleSkin({
required String url,
required double width,
required double height,
required AlignmentGeometry alignment,
}) {
return OverflowBox(
alignment: alignment,
minWidth: width,
maxWidth: width,
minHeight: height,
maxHeight: height,
child:
SCNetworkSvgaWidget.isSvga(url)
? SCNetworkSvgaWidget(
resource: url,
width: width,
height: height,
fit: BoxFit.fill,
loop: true,
centerSliceRatio: _vipBubbleCenterSliceRatio,
sourceEdgeInset: _vipBubbleSourceEdgeInset,
fallback: SizedBox(width: width, height: height),
)
: _buildStaticVipBubbleImage(
url: url,
width: width,
height: height,
fallback: SizedBox(width: width, height: height),
),
);
}
Widget _buildVipBubbleBackground({
required String url,
required double width,
required double height,
required Widget fallback,
}) {
if (SCNetworkSvgaWidget.isSvga(url)) {
return SCNetworkSvgaWidget(
resource: url,
width: width,
height: height,
fit: BoxFit.fill,
loop: true,
centerSliceRatio: _vipBubbleCenterSliceRatio,
sourceEdgeInset: _vipBubbleSourceEdgeInset,
fallback: fallback,
);
}
return _buildStaticVipBubbleImage(
url: url,
width: width,
height: height,
fallback: fallback,
);
}
Widget _buildStaticVipBubbleImage({
required String url,
required double width,
required double height,
Widget? fallback,
}) {
final imageUrl = url.trim();
if (imageUrl.isEmpty) {
return fallback ?? SizedBox(width: width, height: height);
}
return SCNinePatchImage(
url: imageUrl,
width: width,
height: height,
centerSliceRatio: _vipBubbleCenterSliceRatio,
sourceEdgeInset: _vipBubbleSourceEdgeInset,
fallback: fallback ?? SizedBox(width: width, height: height),
);
}
Size _measureMessageTextSize({
required String text,
required TextStyle style,
required double maxWidth,
}) {
final textPainter = TextPainter(
text: TextSpan(text: text, style: style),
textDirection: Directionality.of(context),
)..layout(maxWidth: maxWidth);
return textPainter.size;
}
String _resolveRoomChatBubbleImageUrl(PropsResources? resource) {
final candidates = <String?>[
resource?.roomSendCoverUrl,
resource?.roomOpenedUrl,
resource?.roomNotOpenedUrl,
resource?.imSendCoverUrl,
resource?.imOpenedUrl,
resource?.imOpenedUrlTwo,
resource?.imNotOpenedUrl,
resource?.cover,
resource?.expand,
resource?.sourceUrl,
];
return _firstNonBlank([
...candidates.where((value) => SCNetworkSvgaWidget.isSvga(value)),
...candidates,
]);
}
String _firstNonBlank(Iterable<String?> values) {
for (final value in values) {
final text = value?.trim();
if (text != null && text.isNotEmpty) {
return text;
}
}
return "";
}
///系统提示
_systemTipsRoomItem(BuildContext context) {
if (widget.msg.type == SCRoomMsgType.cpSystemNotice) {
return Container(
alignment: AlignmentDirectional.topStart,
child: Container(
width: 315.w,
constraints: BoxConstraints(minHeight: 49.w),
padding: EdgeInsetsDirectional.fromSTEB(28.w, 6.w, 18.w, 6.w),
margin: EdgeInsets.symmetric(horizontal: 10.w).copyWith(bottom: 8.w),
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(_cpSystemNoticeBubbleAsset),
fit: BoxFit.fill,
centerSlice: _cpSystemNoticeBubbleCenterSlice,
),
),
child: Align(
alignment: Alignment.center,
child: Text(
widget.msg.msg ?? '',
maxLines: 4,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: sp(12),
height: 1.35,
color: Color(_msgColor(widget.msg.type ?? "")),
fontWeight: FontWeight.w500,
decoration: TextDecoration.none,
),
textAlign: TextAlign.center,
),
),
),
);
}
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: Colors.black38,
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,
),
),
);
}
///火箭用户中奖
_buildRocketRewardMsg(BuildContext context) {
List<InlineSpan> spans = [];
final nickname = Msg._firstNonBlankString([
widget.msg.user?.userNickname,
widget.msg.user?.account,
widget.msg.user?.id,
SCAppLocalizations.of(context)?.member,
'Member',
]);
spans.add(
TextSpan(
text: "$nickname ",
style: TextStyle(
fontSize: sp(12),
color: SocialChatTheme.primaryColor,
fontWeight: FontWeight.w500,
),
recognizer: TapGestureRecognizer()..onTap = () {},
),
);
spans.add(
TextSpan(
text: SCAppLocalizations.of(context)!.inRocket,
style: TextStyle(
fontSize: sp(12),
color: Colors.white,
fontWeight: FontWeight.w500,
),
recognizer: TapGestureRecognizer()..onTap = () {},
),
);
spans.add(
WidgetSpan(alignment: PlaceholderAlignment.middle, child: _rocketIcon()),
);
spans.add(
TextSpan(
text: SCAppLocalizations.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 == SCActivityRewardATPropsType.GOLD.name
? Image.asset("sc_images/general/sc_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: Colors.black38,
borderRadius: BorderRadius.all(Radius.circular(15.w)),
),
child: text,
),
);
}
Widget _rocketIcon() {
final iconUrl = widget.msg.rocketIconUrl?.trim() ?? '';
if (iconUrl.isEmpty) {
return SizedBox(width: 4.w);
}
return netImage(url: iconUrl, width: 18.w);
}
Widget _buildRocketLaunchNoticeMsg(BuildContext context) {
final seconds =
(widget.msg.number ?? num.tryParse(widget.msg.msg ?? '') ?? 10).toInt();
final template =
SCAppLocalizations.of(
context,
)?.roomRocketFuelFullNotice(seconds.toString()) ??
'Rocket fuel is full! Launching in ${seconds}s!';
final iconUrl = widget.msg.rocketIconUrl?.trim() ?? '';
return Container(
alignment: AlignmentDirectional.topStart,
child: Container(
constraints: BoxConstraints(minHeight: 42.w, maxWidth: 304.w),
padding: EdgeInsetsDirectional.only(
start: 12.w,
end: 10.w,
top: 8.w,
bottom: 8.w,
),
margin: EdgeInsetsDirectional.only(start: 10.w, end: 36.w, bottom: 8.w),
decoration: BoxDecoration(
color: const Color(0xFFF1F2FF).withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(9.w),
border: Border.all(color: const Color(0xFF6672FF), width: 1.w),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Text(
template,
textScaler: TextScaler.noScaling,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: const Color(0xFF6672FF),
fontSize: sp(13),
height: 1.2,
fontWeight: FontWeight.w600,
),
),
),
SizedBox(width: 8.w),
SizedBox(
width: 42.w,
height: 28.w,
child:
iconUrl.isEmpty
? Icon(
Icons.rocket_launch_rounded,
color: const Color(0xFFB8BEFA),
size: 28.w,
)
: netImage(
url: iconUrl,
width: 42.w,
height: 28.w,
fit: BoxFit.contain,
noDefaultImg: true,
),
),
],
),
),
);
}
///幸运礼物
_buildGameLuckyGiftMsg(BuildContext context) {
List<InlineSpan> spans = [];
spans.add(
TextSpan(
text: "${widget.msg.user?.userNickname} ",
style: TextStyle(
fontSize: sp(12),
color: SocialChatTheme.primaryColor,
fontWeight: FontWeight.w500,
),
recognizer:
TapGestureRecognizer()
..onTap = () {
widget.onClick(widget.msg.user);
},
),
);
spans.add(
TextSpan(
text: SCAppLocalizations.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: SocialChatTheme.primaryColor,
fontWeight: FontWeight.w500,
),
recognizer:
TapGestureRecognizer()
..onTap = () {
widget.onClick(widget.msg.toUser);
},
),
);
SCGlobalConfig.lang == "ar"
? spans.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Image.asset("sc_images/general/sc_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: " ${SCAppLocalizations.of(context)!.obtain}",
style: TextStyle(
fontSize: sp(12),
color: SocialChatTheme.primaryColor,
fontWeight: FontWeight.w500,
),
recognizer: TapGestureRecognizer()..onTap = () {},
),
);
spans.add(
TextSpan(
text: " ${_formatAwardAmount(widget.msg.awardAmount)}",
style: TextStyle(
fontSize: sp(12),
color: SocialChatTheme.primaryColor,
fontWeight: FontWeight.w500,
),
recognizer: TapGestureRecognizer()..onTap = () {},
),
);
SCGlobalConfig.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("sc_images/general/sc_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: Colors.black38,
borderRadius: BorderRadius.all(Radius.circular(15.w)),
),
child: text,
),
);
}
///幸运礼物中奖5倍以及以上
_buildGameLuckyGiftMsg_5(BuildContext context) {
List<InlineSpan> 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: SCAppLocalizations.of(context)!.get,
style: TextStyle(
fontSize: sp(12),
color: Colors.white,
fontWeight: FontWeight.w500,
),
recognizer: TapGestureRecognizer()..onTap = () {},
),
);
spans.add(
TextSpan(
text: " ${_formatAwardAmount(widget.msg.awardAmount)} ",
style: TextStyle(
fontSize: sp(12),
color: Color(0xffFEF129),
fontWeight: FontWeight.w500,
),
recognizer: TapGestureRecognizer()..onTap = () {},
),
);
spans.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Image.asset(
"sc_images/general/sc_icon_jb.png",
width: 18.w,
height: 18.w,
),
),
);
spans.add(
TextSpan(
text: " from ",
style: TextStyle(
fontSize: sp(12),
color: Colors.white,
fontWeight: FontWeight.w500,
),
recognizer: TapGestureRecognizer()..onTap = () {},
),
);
spans.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: netImage(
url: widget.msg.gift?.giftPhoto ?? "",
width: 18.w,
height: 18.w,
),
),
);
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("sc_images/room/sc_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(
"sc_images/room/sc_icon_luck_gift_msg_n_ball.png",
),
fit: BoxFit.fill,
),
),
alignment: AlignmentDirectional.center,
width: 55.w,
height: 60.w,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 10.w),
buildNumForGame(widget.msg.msg ?? "0", size: 18.w),
SizedBox(height: 3.w),
SCGlobalConfig.lang == "ar"
? Image.asset(
"sc_images/room/sc_icon_times_text_ar.png",
height: 12.w,
)
: Image.asset(
"sc_images/room/sc_icon_times_text_en.png",
height: 10.w,
),
],
),
),
SizedBox(width: 15.w),
],
),
),
);
}
String _formatAwardAmount(num? awardAmount) {
if (awardAmount == null) {
return "0";
}
if (awardAmount % 1 == 0) {
return awardAmount.toInt().toString();
}
return awardAmount.toString();
}
Widget _buildGiftMsg(BuildContext context) {
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: Container(
constraints: BoxConstraints(
maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束
),
decoration: BoxDecoration(
color: Colors.black38,
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: SCAppLocalizations.of(context)!.sendTo,
style: TextStyle(
fontSize: 13.sp,
color: Colors.white,
),
),
TextSpan(text: " "),
TextSpan(
text: _giftTargetName(context),
style: TextStyle(
fontSize: 13.sp,
color: SocialChatTheme.primaryColor,
),
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,
),
),
),
],
),
],
),
);
}
String _giftTargetName(BuildContext context) {
final targetUserIds =
widget.msg.targetUserIds
?.map((id) => id.trim())
.where((id) => id.isNotEmpty)
.toSet();
if (targetUserIds != null && targetUserIds.length > 1) {
return SCAppLocalizations.of(context)!.multiple;
}
return widget.msg.toUser?.userNickname ?? "";
}
///进入房间
_enterRoomItem(BuildContext context) {
List<InlineSpan> 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: SCAppLocalizations.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 _buildUserNameAndLevels(SocialChatUserProfile? user) {
return SizedBox(
height: 23.w,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
fit: FlexFit.loose,
child: socialchatNickNameText(
user?.userNickname ?? "",
maxWidth: 112.w,
fontWeight: FontWeight.w500,
fontSize: 13.sp,
type: user?.getVIP()?.name ?? "",
needScroll: (user?.userNickname?.characters.length ?? 0) > 10,
),
),
SizedBox(width: 4.w),
getWealthLevel(
user?.wealthLevel ?? 0,
width: 42.w,
height: 20.w,
fontSize: 9.sp,
),
SizedBox(width: 2.w),
getUserLevel(
user?.charmLevel ?? 0,
width: 42.w,
height: 20.w,
fontSize: 9.sp,
),
],
),
);
}
List<WearBadge> _activeWearBadges(SocialChatUserProfile? user) {
return user?.wearBadge?.where((item) => item.use ?? false).toList() ??
const <WearBadge>[];
}
Widget _buildMedals({
required String? userId,
List<WearBadge> fallbackBadges = const <WearBadge>[],
}) {
final hasUserId = (userId ?? "").trim().isNotEmpty;
if (!hasUserId && fallbackBadges.isEmpty) {
return SizedBox(height: 16.w);
}
return SizedBox(
width: double.infinity,
child: SCUserBadgeStrip(
userId: userId,
fallbackBadges: fallbackBadges,
height: 28.w,
badgeHeight: 25.w,
longBadgeWidth: 51.w,
spacing: 5.w,
maxBadges: 8,
reserveSpace: true,
),
);
}
Widget _buildLongMedals({
required String? userId,
List<WearBadge> fallbackBadges = const <WearBadge>[],
}) {
final hasUserId = (userId ?? "").trim().isNotEmpty;
if (!hasUserId && fallbackBadges.isEmpty) {
return const SizedBox.shrink();
}
return SizedBox(
width: double.infinity,
child: SCUserBadgeStrip(
userId: userId,
fallbackBadges: fallbackBadges,
displayScope: SCUserBadgeDisplayScope.long,
height: 18.w,
badgeHeight: 16.w,
longBadgeWidth: 50.w,
spacing: 3.w,
maxBadges: 8,
reserveSpace: false,
),
);
}
int _msgColor(String type) {
switch (type) {
case SCRoomMsgType.joinRoom:
case SCRoomMsgType.systemTips:
case SCRoomMsgType.welcome:
return 0xFFFFFFFF;
case SCRoomMsgType.cpSystemNotice:
return 0xFF6F536D;
case SCRoomMsgType.gift:
return 0xFFFFFFFF;
case SCRoomMsgType.shangMai:
case SCRoomMsgType.xiaMai:
case SCRoomMsgType.qcfj:
case SCRoomMsgType.killXiaMai:
return 0xFFFFFFFF;
// case type.text:
// return _mapTextColor();
// break;
default:
return 0xFFFFFFFF;
}
}
_roleNameColor(String role) {
if (SCRoomRolesType.HOMEOWNER.name == role) {
return Color(0xff2AC0F2);
} else if (SCRoomRolesType.ADMIN.name == role) {
return Color(0xffF9A024);
} else if (SCRoomRolesType.MEMBER.name == role) {
return Color(0xff28ED77);
} else {
return Colors.white;
}
}
Widget _buildRoleChangeMsg(BuildContext context) {
var text = "";
if (SCRoomRolesType.ADMIN.name == widget.msg.msg) {
text = SCAppLocalizations.of(context)!.adminByHomeowner;
} else if (SCRoomRolesType.MEMBER.name == widget.msg.msg) {
text = SCAppLocalizations.of(context)!.memberByHomeowner;
} else {
text = SCAppLocalizations.of(context)!.touristByHomeowner;
}
List<InlineSpan> 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: Colors.black38,
borderRadius: BorderRadius.all(Radius.circular(15.w)),
),
child: textView,
),
);
}
Widget _buildImageMsg(BuildContext context) {
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: Container(
constraints: BoxConstraints(
maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束
),
decoration: BoxDecoration(
color: Colors.black38,
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]),
);
SCNavigatorUtils.push(
context,
"${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0",
);
},
),
),
],
),
],
),
),
);
}
Widget _buildEmoticonMsg(BuildContext context) {
final emojiPath = widget.msg.msg ?? "";
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(
child: Container(
constraints: BoxConstraints(
maxWidth: ScreenUtil().screenWidth * 0.45,
),
decoration: BoxDecoration(
color: Colors.black38,
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: 8.w,
),
margin: EdgeInsetsDirectional.only(
start: 54.w,
).copyWith(bottom: 8.w),
child: _buildEmojiImage(
emojiPath,
width: 72.w,
height: 72.w,
),
),
),
],
),
],
),
),
);
}
Widget _buildEmojiImage(
String path, {
required double width,
required double height,
}) {
if (path.isEmpty) {
return SizedBox(width: width, height: height);
}
return RoomEmojiAssetImage(
key: ValueKey(path),
asset: path,
width: width,
height: height,
);
}
Widget _buildDiceMsg(BuildContext context) {
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: Container(
constraints: BoxConstraints(
maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束
),
decoration: BoxDecoration(
color: Colors.black38,
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(
"sc_images/room/sc_icon_dice_${widget.msg.msg}.png",
height: 35.w,
)
: FutureBuilder<void>(
future: Future.delayed(
Duration(milliseconds: 2000),
),
// 传入Future
builder: (
BuildContext context,
AsyncSnapshot<void> snapshot,
) {
// 根据snapshot的状态来构建UI
if (snapshot.connectionState ==
ConnectionState.done) {
widget.msg.hasPlayedAnimation = true;
return Image.asset(
"sc_images/room/sc_icon_dice_${widget.msg.msg}.png",
height: 35.w,
);
} else {
return Image.asset(
"sc_images/room/sc_icon_dice_animl.webp",
height: 35.w,
);
}
},
),
),
onTap: () {},
),
),
],
),
],
),
),
);
}
Widget _buildRPSMsg(BuildContext context) {
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: Container(
constraints: BoxConstraints(
maxWidth: ScreenUtil().screenWidth * 0.5, // 最大宽度约束
),
decoration: BoxDecoration(
color: Colors.black38,
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(
"sc_images/room/sc_icon_rps_${widget.msg.msg}.png",
height: 35.w,
)
: FutureBuilder<void>(
future: Future.delayed(
Duration(milliseconds: 2000),
),
// 传入Future
builder: (
BuildContext context,
AsyncSnapshot<void> snapshot,
) {
// 根据snapshot的状态来构建UI
if (snapshot.connectionState ==
ConnectionState.done) {
widget.msg.hasPlayedAnimation = true;
return Image.asset(
"sc_images/room/sc_icon_rps_${widget.msg.msg}.png",
height: 35.w,
);
} else {
return Image.asset(
"sc_images/room/sc_icon_rps_animal.webp",
height: 35.w,
);
}
},
),
),
onTap: () {},
),
),
],
),
],
),
),
);
}
Widget _buildLuckNumberMsg(BuildContext context) {
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: Container(
constraints: BoxConstraints(
maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束
),
decoration: BoxDecoration(
color: Colors.black38,
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: [
SCGlobalConfig.lang == "ar"
? Image.asset(
"sc_images/room/sc_icon_luck_num_text_ar.png",
height: 20.w,
)
: Image.asset(
"sc_images/room/sc_icon_luck_num_text_en.png",
height: 20.w,
),
buildNumForRoomLuckNum(
widget.msg.msg ?? "",
size: 20.w,
),
],
),
),
onTap: () {},
),
),
],
),
],
),
),
);
}
_buildUserInfoLine(BuildContext context) {
final userId = widget.msg.user?.id;
if ((widget.msg.needUpDataUserInfo) ?? false) {
SCRoomUtils.roomUsersMap.remove(userId ?? "");
if ((userId ?? "").isNotEmpty) {
_userInfoFutures.remove(userId);
_userInfoFailedAt.remove(userId);
}
widget.msg.needUpDataUserInfo = false;
}
if (userId == null || userId.isEmpty) {
return _buildPendingUserInfoLine(context);
}
return SCRoomUtils.roomUsersMap[userId] == null
? FutureBuilder<SocialChatUserProfile?>(
future: _loadRoomUserInfoSilently(userId),
builder: (ct, AsyncSnapshot<SocialChatUserProfile?> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
SocialChatUserProfile user = snapshot.data!;
SCRoomUtils.roomUsersMap[user.id ?? ""] = user;
return _buildNewUserInfoLine(context, user);
}
}
return _buildPendingUserInfoLine(context);
},
)
: _buildNewUserInfoLine(context, SCRoomUtils.roomUsersMap[userId]!);
}
Future<SocialChatUserProfile?> _loadRoomUserInfoSilently(String userId) {
final failedAt = _userInfoFailedAt[userId];
if (failedAt != null &&
DateTime.now().difference(failedAt) < _userInfoFailureRetryInterval) {
return Future.value(null);
}
return _userInfoFutures.putIfAbsent(userId, () async {
try {
final user = await SCAccountRepository().loadUserInfo(
userId,
silentErrorToast: true,
);
SCRoomUtils.roomUsersMap[user.id ?? userId] = user;
_userInfoFailedAt.remove(userId);
return user;
} catch (error) {
_userInfoFailedAt[userId] = DateTime.now();
_userInfoFutures.remove(userId);
return null;
}
});
}
Widget _buildPendingUserInfoLine(BuildContext context) {
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: 8.w),
Expanded(
child: SizedBox(
height: 76.w,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildUserNameAndLevels(widget.msg.user),
SizedBox(height: 2.w),
_buildLongMedals(
userId: widget.msg.user?.id,
fallbackBadges: _activeWearBadges(widget.msg.user),
),
SizedBox(height: 2.w),
_buildMedals(
userId: widget.msg.user?.id,
fallbackBadges: _activeWearBadges(widget.msg.user),
),
],
),
),
),
],
);
}
_buildNewUserInfoLine(BuildContext context, SocialChatUserProfile 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: 8.w),
Expanded(
child: SizedBox(
height: 76.w,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildUserNameAndLevels(user),
SizedBox(height: 2.w),
_buildLongMedals(
userId: user.id,
fallbackBadges: _activeWearBadges(user),
),
SizedBox(height: 2.w),
_buildMedals(
userId: user.id,
fallbackBadges: _activeWearBadges(user),
),
],
),
),
),
],
);
}
}
///消息
class Msg {
String? groupId;
String? msg;
MicRes? userWheat;
String? type;
String? noticeAction;
SocialChatUserProfile? user;
String? role = "";
String? rocketIconUrl = "";
SocialChatGiftRes? gift;
//礼物个数
num? number;
// 自定义动画需要播放的次数
int? customAnimationCount;
num? awardAmount;
// 送礼给谁
SocialChatUserProfile? toUser;
List<String>? targetUserIds;
String? giftBatchId;
int? time = 0;
//动画是否已经播放(骰子、石头剪刀布、随机数字)
bool? hasPlayedAnimation = false;
bool? needUpDataUserInfo = false;
Msg({
required this.groupId,
required this.msg,
required this.type,
this.noticeAction,
this.user,
this.toUser,
this.userWheat,
this.role = "",
this.rocketIconUrl,
this.gift,
this.number,
this.customAnimationCount,
this.awardAmount,
this.targetUserIds,
this.giftBatchId,
this.hasPlayedAnimation,
this.needUpDataUserInfo,
}) {
time = DateTime.now().millisecondsSinceEpoch;
}
Msg.fromJson(dynamic json) {
groupId = json['groupId'];
msg = json['msg'];
type = json['type'];
noticeAction = json['noticeAction']?.toString();
role = json['role'];
rocketIconUrl = json['rocketIconUrl']?.toString();
time = json['time'];
number = json['number'];
customAnimationCount = json['customAnimationCount'];
awardAmount = json['awardAmount'];
giftBatchId = json['giftBatchId']?.toString();
final dynamic rawTargetUserIds = json['targetUserIds'];
if (rawTargetUserIds is List) {
targetUserIds =
rawTargetUserIds
.map((item) => item?.toString().trim() ?? "")
.where((id) => id.isNotEmpty)
.toList();
}
hasPlayedAnimation = json['hasPlayedAnimation'];
needUpDataUserInfo = json['needUpDataUserInfo'];
userWheat =
json['userWheat'] != null ? MicRes.fromJson(json['userWheat']) : null;
user =
json['user'] != null
? SocialChatUserProfile.fromJson(json['user'])
: null;
toUser =
json['toUser'] != null
? SocialChatUserProfile.fromJson(json['toUser'])
: null;
gift =
json['gift'] != null ? SocialChatGiftRes.fromJson(json['gift']) : null;
_hydrateRocketRewardPayload(json);
}
void _hydrateRocketRewardPayload(dynamic json) {
if (!SCRoomMsgType.isRocketRewardUserType(type)) {
return;
}
final root = _stringKeyMap(json);
final payload = _stringKeyMap(root['data']);
final source = payload.isEmpty ? root : payload;
groupId = _firstNonBlankString([
groupId,
source['groupId'],
source['roomAccount'],
source['room_account'],
source['roomId'],
source['room_id'],
]);
role = _firstNonBlankString([
role,
source['rewardType'],
source['reward_type'],
]);
rocketIconUrl = _firstNonBlankString([
rocketIconUrl,
source['rocketIconUrl'],
source['rocket_icon_url'],
source['rocketIcon'],
source['rewardCover'],
source['reward_cover'],
]);
msg = _firstNonBlankString([
msg,
source['rewardCover'],
source['reward_cover'],
source['rewardIconUrl'],
source['reward_icon_url'],
source['cover'],
rocketIconUrl,
]);
user ??= _rocketRewardUserProfile(source);
}
static SocialChatUserProfile? _rocketRewardUserProfile(
Map<String, dynamic> source,
) {
final userId = _firstNonBlankString([
source['userId'],
source['user_id'],
source['rewardUserId'],
source['reward_user_id'],
source['receiverUserId'],
source['receiver_user_id'],
source['targetUserId'],
source['target_user_id'],
]);
final nickname = _firstNonBlankString([
source['userNickname'],
source['user_nickname'],
source['nickname'],
source['receiverNickname'],
source['receiver_nickname'],
source['account'],
userId,
]);
final account = _firstNonBlankString([
source['account'],
source['userAccount'],
source['user_account'],
source['actualAccount'],
source['actual_account'],
userId,
]);
final avatar = _firstNonBlankString([
source['userAvatar'],
source['user_avatar'],
source['avatar'],
source['receiverAvatar'],
source['receiver_avatar'],
]);
if (userId.isEmpty &&
nickname.isEmpty &&
account.isEmpty &&
avatar.isEmpty) {
return null;
}
return SocialChatUserProfile(
id: userId,
account: account,
userNickname: nickname,
userAvatar: avatar,
);
}
static Map<String, dynamic> _stringKeyMap(dynamic value) {
if (value is Map<String, dynamic>) {
return value;
}
if (value is Map) {
return value.map((key, item) => MapEntry(key.toString(), item));
}
return const <String, dynamic>{};
}
static String _firstNonBlankString(Iterable<dynamic> values) {
for (final value in values) {
final text = value?.toString().trim() ?? '';
if (text.isNotEmpty && text.toLowerCase() != 'null') {
return text;
}
}
return '';
}
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['groupId'] = groupId;
map['msg'] = msg;
map['type'] = type;
if ((noticeAction ?? "").trim().isNotEmpty) {
map['noticeAction'] = noticeAction;
}
map['role'] = role;
if ((rocketIconUrl ?? "").trim().isNotEmpty) {
map['rocketIconUrl'] = rocketIconUrl;
}
map['time'] = time;
map['awardAmount'] = awardAmount;
map['number'] = number;
map['customAnimationCount'] = customAnimationCount;
map['hasPlayedAnimation'] = hasPlayedAnimation;
map['needUpDataUserInfo'] = needUpDataUserInfo;
if ((giftBatchId ?? "").trim().isNotEmpty) {
map['giftBatchId'] = giftBatchId;
}
if (targetUserIds != null) {
map['targetUserIds'] = targetUserIds;
}
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;
}
}