import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; import 'package:extended_image/extended_image.dart' show ExtendedImage, ExtendedRawImage, ExtendedImageState, LoadState; import 'package:extended_text/extended_text.dart'; import 'package:extended_text_field/extended_text_field.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/app/constants/sc_emoji_datas.dart'; import 'package:yumi/shared/tools/sc_date_utils.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/shared/tools/sc_message_utils.dart'; import 'package:yumi/shared/tools/sc_user_id_utils.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:marquee/marquee.dart'; import 'package:provider/provider.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart'; import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart'; import 'package:tencent_cloud_chat_sdk/enum/message_status.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_custom_elem.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_elem.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_image_elem.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_extension.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_online_url.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_text_elem.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_video_elem.dart'; import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; import 'package:yumi/config/pickImage.dart'; import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/config/business_logic_strategy.dart'; import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart'; import 'package:yumi/shared/tools/sc_keybord_util.dart'; import 'package:yumi/shared/tools/sc_message_notifier.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart'; import 'package:yumi/services/auth/user_profile_manager.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/room/cp/room_cp_invite_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/cp/room_cp_relation_formed_dialog.dart'; import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart'; import '../../shared/business_logic/models/res/sc_system_invit_message_res.dart'; import '../../shared/business_logic/models/res/sc_user_red_packet_send_res.dart'; import '../../shared/data_sources/models/enum/sc_sysytem_message_type.dart'; enum _CpInviteMessageState { pending, expired, rejected, accepted } const EdgeInsets _vipBubbleCenterSliceRatio = EdgeInsets.fromLTRB( 0.34, 0.34, 0.34, 0.34, ); const EdgeInsets _vipBubbleSourceEdgeInset = EdgeInsets.all(2); String _resolveVipChatBubbleImageUrl(SCVipResourceRes? resource) { final candidates = [ resource?.sourceResourceUrl, resource?.previewUrl, resource?.coverUrl, resource?.cover, ]; return _firstNonBlankChatBubbleUrl([ ...candidates.where((value) => SCNetworkSvgaWidget.isSvga(value)), ...candidates, ]); } String _firstNonBlankChatBubbleUrl(Iterable values) { for (final value in values) { final text = value?.trim(); if (text != null && text.isNotEmpty) { return text; } } return ""; } class SCMessageChatPage extends StatefulWidget { final V2TimConversation? conversation; final bool shrinkWrap; //是否是房间内聊天界面。 final bool inRoom; const SCMessageChatPage({ Key? key, this.conversation, this.shrinkWrap = false, this.inRoom = false, }) : super(key: key); @override _SCMessageChatPageState createState() => _SCMessageChatPageState(); } class _SCMessageChatPageState extends State { BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; final TextEditingController _textController = TextEditingController(); final ScrollController _scrollController = ScrollController(); final RefreshController _refreshController = RefreshController(); // FlutterSoundPlayer? flutterSound; RtmProvider? rtmProvider; V2TimConversation? currentConversation; List currentConversationMessageList = []; final FocusNode _focusNode = FocusNode(); ///是否显示工具栏 bool showTools = false; bool showEmoji = false; bool showDoNotClickUnfamiliarTips = true; ///互相关注才能互相发送消息 bool canSendMsg = false; SocialChatUserProfile? friend; SCVipResourceRes? _selfVipChatBubble; bool _isSelfVipChatBubbleLoading = false; final Map _localCpInviteStates = {}; ///是否显示发送按钮 bool showSend = false; List coinsTitles = ["100", "1000", "10000", "50000"]; String selecteCoins = "100"; bool get _isSystemConversation => SCGlobalConfig.isSystemConversationId( currentConversation?.conversationID, ) || SCGlobalConfig.isSystemUserId(currentConversation?.userID); bool get _isC2CConversation => currentConversation?.type == ConversationType.V2TIM_C2C; bool get _isGroupConversation => currentConversation?.type == ConversationType.V2TIM_GROUP; bool get _canOpenFriendProfile => _isC2CConversation && friend != null; bool get _canSendConversationMessage => _isC2CConversation && !_isSystemConversation; String get _conversationTitle { final friendNickname = friend?.userNickname ?? ""; if (friendNickname.isNotEmpty) { return friendNickname; } return currentConversation?.showName ?? currentConversation?.userID ?? currentConversation?.groupID ?? ""; } @override void initState() { super.initState(); SCMessageNotifier.canPlay = false; SCMessageUtils.redPacketFutureCache.clear(); // flutterSound = FlutterSoundPlayer(); rtmProvider = Provider.of(context, listen: false); currentConversation = widget.conversation; rtmProvider?.onMessageRecvC2CReadListener = _onMessageRecvC2CRead; rtmProvider?.onRevokeMessageListener = _onRevokeMessage; rtmProvider?.onNewMessageCurrentConversationListener = _onNewMessage; loadMsg(); _loadSelfVipChatBubble(); if (_isC2CConversation && friend == null) { loadFriend(); } _focusNode.addListener(() { if (_focusNode.hasFocus) { setState(() { showTools = false; showEmoji = false; }); } }); } @override void dispose() { rtmProvider?.onMessageRecvC2CReadListener = null; rtmProvider?.onRevokeMessageListener = null; rtmProvider?.onNewMessageCurrentConversationListener = null; _textController.dispose(); _scrollController.dispose(); _refreshController.dispose(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Consumer( builder: (_, provider, __) { return WillPopScope( onWillPop: () { if (showEmoji || showTools) { setState(() { showEmoji = false; showTools = false; }); return Future.value(false); } SCNavigatorUtils.goBack(context); return Future.value(true); }, child: Stack( children: [ if (!widget.inRoom) // 渐变背景 Positioned( left: 0, right: 0, top: 0, child: Column( children: [ SizedBox( width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, child: Image.asset( _strategy.getSCMessageChatPageRoomSettingBackground(), fit: BoxFit.fill, ), ), ], ), ), Scaffold( //backgroundColor: widget.inRoom ? Colors.transparent: Color(0xfff8f8f8), backgroundColor: Colors.transparent, // appBar: _appBar(), appBar: null, body: SafeArea( top: widget.shrinkWrap ? false : true, bottom: false, child: Column( children: [ Row( children: [ !widget.inRoom ? GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { Navigator.pop(context); }, child: Container( padding: EdgeInsets.symmetric( horizontal: 10.w, vertical: 10.w, ), child: Icon( SCGlobalConfig.lang == "ar" ? Icons.keyboard_arrow_right : Icons.keyboard_arrow_left, size: 28.w, color: Colors.white, ), ), ) : Container(), SizedBox(width: 5.w), Expanded( child: Container( alignment: AlignmentDirectional.center, child: socialchatNickNameText( _conversationTitle, fontSize: 14.sp, fontWeight: FontWeight.w500, textColor: Colors.white, type: "", needScroll: _conversationTitle.characters.length > 22, ), ), ), if (_canOpenFriendProfile) GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { SCNavigatorUtils.push( context, replace: true, "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == friend?.id}&tageId=${friend?.id}", ); }, child: Container( padding: EdgeInsets.symmetric(horizontal: 10.w), child: Icon( Icons.more_vert, size: 22.w, color: Colors.white, ), ), ), ], ), Expanded(child: _msgList()), Container( decoration: BoxDecoration( //color: Colors.white, //border: Border(top: BorderSide(color: Color(0xfff1f1f1))) ), child: SafeArea( top: false, child: Column( children: [ if (_canSendConversationMessage) _input(), _tools(provider), _emoji(), // _fahongbao(), ], ), ), ), ], ), ), ), ], ), ); }, ); } void loadFriend() { final userId = currentConversation?.userID; if (!_isC2CConversation || _isSystemConversation || SCGlobalConfig.isSystemUserId(userId) || userId == null || userId.isEmpty || !scLooksLikeBackendUserId(userId)) { return; } SCLoadingManager.show(); Future.wait([ SCAccountRepository().loadUserInfo(userId, silentErrorToast: true), SCAccountRepository().friendRelationCheck(userId), ]) .then((result) { SCLoadingManager.hide(); friend = result[0] as SocialChatUserProfile; canSendMsg = result[1] as bool; setState(() {}); }) .catchError((e) { SCLoadingManager.hide(); }); } Future _loadSelfVipChatBubble() async { if (_isSelfVipChatBubbleLoading) return; final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id; if (currentUserId == null || currentUserId.isEmpty) return; _isSelfVipChatBubbleLoading = true; final repository = SCVipRepositoryImp(); SCVipHomeRes? vipHome; SCVipResourceRes? chatBubble; try { final home = await repository.vipHome(); vipHome = home; chatBubble = _resolveSelfVipChatBubbleFromHome(home); } catch (error) {} if (chatBubble == null) { try { final status = await repository.vipStatus(); chatBubble = _resolveSelfVipChatBubbleFromStatus(status); if (chatBubble == null && vipHome != null && _isUsableVipStatus(status)) { chatBubble = _firstUsableVipChatBubble([ _currentVipLevelConfig( vipHome, targetLevel: status.levelInt, targetLevelCode: status.levelCode, )?.chatBubble, ]); } } catch (error) {} } if (!mounted) return; setState(() { _selfVipChatBubble = chatBubble; _isSelfVipChatBubbleLoading = false; }); } SCVipResourceRes? _resolveSelfVipChatBubbleFromHome(SCVipHomeRes home) { final status = home.state; if (status?.active == false) { return null; } final config = _currentVipLevelConfig(home); if (_isUsableVipStatus(status)) { return _firstUsableVipChatBubble([ status?.chatBubble, config?.chatBubble, ]); } final profileLevel = AccountStorage().getCurrentUser()?.userProfile?.vipLevelForColoredId ?? 0; if (profileLevel > 0) { return _firstUsableVipChatBubble([config?.chatBubble]); } return null; } SCVipResourceRes? _resolveSelfVipChatBubbleFromStatus(SCVipStatusRes status) { if (!_isUsableVipStatus(status)) { return null; } return _firstUsableVipChatBubble([status.chatBubble]); } SCVipLevelConfigRes? _currentVipLevelConfig( SCVipHomeRes home, { int? targetLevel, String? targetLevelCode, }) { final status = home.state; final statusLevel = _vipLevelIntFromStatus(status); final profileLevel = AccountStorage().getCurrentUser()?.userProfile?.vipLevelForColoredId ?? 0; final resolvedTargetLevel = (targetLevel ?? 0) > 0 ? targetLevel! : (statusLevel > 0 ? statusLevel : profileLevel); final statusLevelCode = targetLevelCode?.trim() ?? status?.levelCode?.trim(); for (final level in home.levels) { if (resolvedTargetLevel > 0 && level.levelInt == resolvedTargetLevel) { return level; } } if (statusLevelCode != null && statusLevelCode.isNotEmpty) { for (final level in home.levels) { if (_sameVipLevelCode(level.levelCode, statusLevelCode)) { return level; } } } return null; } bool _isUsableVipStatus(SCVipStatusRes? status) { return status != null && status.active != false && (_vipLevelIntFromStatus(status) > 0 || _hasNonBlankText(status.levelCode) || _hasNonBlankText(status.displayName) || _resolveVipChatBubbleImageUrl(status.chatBubble).isNotEmpty); } int _vipLevelIntFromStatus(SCVipStatusRes? status) { final explicitLevel = status?.levelInt ?? 0; if (explicitLevel > 0) { return explicitLevel; } return _parseVipLevelText(status?.levelCode) ?? _parseVipLevelText(status?.displayName) ?? 0; } int? _parseVipLevelText(String? value) { final text = value?.trim(); if (text == null || text.isEmpty) { return null; } final match = RegExp( r'(?:S?VIP)?\s*([1-9]\d*)', caseSensitive: false, ).firstMatch(text); return int.tryParse(match?.group(1) ?? ''); } bool _sameVipLevelCode(String? left, String? right) { final normalizedLeft = left?.replaceAll(RegExp(r'\s+'), '').toLowerCase(); final normalizedRight = right?.replaceAll(RegExp(r'\s+'), '').toLowerCase(); return normalizedLeft != null && normalizedLeft.isNotEmpty && normalizedLeft == normalizedRight; } bool _hasNonBlankText(String? value) { return value?.trim().isNotEmpty ?? false; } SCVipResourceRes? _firstUsableVipChatBubble( Iterable resources, ) { for (final resource in resources) { if (_resolveVipChatBubbleImageUrl(resource).isNotEmpty) { return resource; } } return null; } String _vipChatBubbleLogValue(SCVipResourceRes? resource) { if (resource == null) return 'null'; return '{id:${resource.resourceId},name:${resource.name},' 'previewUrl:${resource.previewUrl},' 'sourceUrl:${resource.sourceResourceUrl},' 'selectedUrl:${_resolveVipChatBubbleImageUrl(resource)}}'; } ///消息列表 Widget _msgList() { return SmartRefresher( enablePullDown: true, enablePullUp: false, onRefresh: () async { if (currentConversationMessageList.isNotEmpty) { final previousMaxScrollExtent = _scrollController.hasClients ? _scrollController.position.maxScrollExtent : 0.0; final messages = await _loadHistoryMessages( count: 30, lastMsgID: currentConversationMessageList.first.msgID, ); _prependHistoryMessages(messages); _refreshController.refreshCompleted(); setState(() {}); _restoreScrollOffsetAfterPrepend(previousMaxScrollExtent); } else { _refreshController.refreshCompleted(); } }, header: CustomHeader( height: 1, builder: (context, mode) { return SizedBox( height: 1, width: 1, child: SizedBox(height: 1, width: 1), ); }, ), controller: _refreshController, child: CustomScrollView( shrinkWrap: true, controller: _scrollController, reverse: false, physics: const ClampingScrollPhysics(), // 禁用回弹效果 slivers: [ SliverList( delegate: SliverChildBuilderDelegate( (c, i) => _MessageItem( message: currentConversationMessageList[i], selfVipChatBubble: _selfVipChatBubble, currentConversation: currentConversation, preMessage: i > 0 ? currentConversationMessageList[i - 1] : null, isSystem: currentConversationMessageList[i].sender == "administrator", friend: friend, currentConversationMessageList: currentConversationMessageList, localCpInviteStates: _localCpInviteStates, onCpInviteStateChanged: _setLocalCpInviteState, updateCall: () { setState(() {}); }, ), childCount: currentConversationMessageList.length, ), ), ], ), ); } void _setLocalCpInviteState(String applyId, _CpInviteMessageState state) { final normalizedApplyId = applyId.trim(); if (normalizedApplyId.isEmpty) { return; } _localCpInviteStates[normalizedApplyId] = state; if (mounted) { setState(() {}); } } List _messagesInDisplayOrder(List messages) { final indexedMessages = messages.asMap().entries.toList(); indexedMessages.sort((left, right) { final timeCompare = (left.value.timestamp ?? 0).compareTo( right.value.timestamp ?? 0, ); if (timeCompare != 0) { return timeCompare; } final leftSeq = int.tryParse(left.value.seq ?? ""); final rightSeq = int.tryParse(right.value.seq ?? ""); if (leftSeq != null && rightSeq != null && leftSeq != rightSeq) { return leftSeq.compareTo(rightSeq); } return left.key.compareTo(right.key); }); return indexedMessages.map((entry) => entry.value).toList(); } bool _hasMessage(V2TimMessage message) { final messageId = message.msgID; if (messageId == null || messageId.isEmpty) { return false; } return currentConversationMessageList.any( (element) => element.msgID == messageId, ); } void _prependHistoryMessages(List messages) { final displayMessages = _messagesInDisplayOrder( messages, ).where((message) => !_hasMessage(message)); currentConversationMessageList.insertAll(0, displayMessages); } void _appendOrReplaceMessage(V2TimMessage message) { final messageId = message.msgID; final index = messageId == null || messageId.isEmpty ? -1 : currentConversationMessageList.indexWhere( (element) => element.msgID == messageId, ); if (index > -1) { currentConversationMessageList[index] = message; return; } currentConversationMessageList.add(message); } void _scrollToLatestMessage({bool animated = true}) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted || !_scrollController.hasClients) { return; } final targetOffset = _scrollController.position.maxScrollExtent; if (targetOffset <= 0) { _scrollController.jumpTo(0); return; } if (animated) { _scrollController.animateTo( targetOffset, duration: const Duration(milliseconds: 220), curve: Curves.easeOut, ); } else { _scrollController.jumpTo(targetOffset); } }); } void _restoreScrollOffsetAfterPrepend(double previousMaxScrollExtent) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted || !_scrollController.hasClients) { return; } final addedExtent = _scrollController.position.maxScrollExtent - previousMaxScrollExtent; if (addedExtent <= 0) { return; } final targetOffset = _scrollController.offset + addedExtent; _scrollController.jumpTo( targetOffset .clamp(0.0, _scrollController.position.maxScrollExtent) .toDouble(), ); }); } Future> _loadHistoryMessages({ required int count, String? lastMsgID, }) async { final messageManager = TencentImSDKPlugin.v2TIMManager.getMessageManager(); if (_isGroupConversation) { final groupId = currentConversation?.groupID; if (groupId == null || groupId.isEmpty) { return []; } final result = await messageManager.getGroupHistoryMessageList( groupID: groupId, count: count, lastMsgID: lastMsgID, ); return result.data ?? []; } final userId = currentConversation?.userID; if (userId == null || userId.isEmpty) { return []; } final result = await messageManager.getC2CHistoryMessageList( userID: userId, count: count, lastMsgID: lastMsgID, ); return result.data ?? []; } Future _markCurrentConversationAsRead() async { final messageManager = TencentImSDKPlugin.v2TIMManager.getMessageManager(); if (_isGroupConversation) { final groupId = currentConversation?.groupID; if (groupId == null || groupId.isEmpty) { return; } await messageManager.markGroupMessageAsRead(groupID: groupId); return; } final userId = currentConversation?.userID; if (userId == null || userId.isEmpty) { return; } await messageManager.markC2CMessageAsRead(userID: userId); } Future loadMsg() async { // 1. 在初始化时,添加高级消息监听器 TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener( listener: V2TimAdvancedMsgListener( // 监听消息扩展更新 onRecvMessageExtensionsChanged: (msgID, extensions) { for (var ext in extensions) { if (ext.extensionKey == "packetID") { String packetID = ext.extensionValue; SCMessageUtils.redPacketFutureCache[packetID] = SCAccountRepository().userRedPacketDetail(packetID); setState(() {}); break; } } }, ), ); final messages = await _loadHistoryMessages(count: 100); currentConversationMessageList.clear(); for (var msg in messages) { if (!msg.isSelf! && msg.isPeerRead!) { TencentImSDKPlugin.v2TIMManager .getMessageManager() .sendMessageReadReceipts(messageIDList: [msg.msgID!]); } } currentConversationMessageList.addAll(_messagesInDisplayOrder(messages)); await _markCurrentConversationAsRead(); setState(() {}); _scrollToLatestMessage(animated: false); } ///输入栏 Widget _input() { return Consumer( builder: (_, provider, __) => Container( color: Colors.transparent, height: 56.w, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(width: 15.w), Expanded( child: Container( height: 34.w, decoration: BoxDecoration( borderRadius: BorderRadius.circular(52.w), color: Color(0xff18F2B1).withOpacity(0.1), ), child: Row( children: [ Expanded( child: ExtendedTextField( controller: _textController, focusNode: _focusNode, onChanged: (s) { setState(() { showSend = _textController.text.isNotEmpty; }); }, onSubmitted: (s) { if (s.isNotEmpty) { sendMsg(s); showSend = false; setState(() {}); _textController.clear(); } }, decoration: InputDecoration( hintText: SCAppLocalizations.of( context, )!.clickHereToStartChatting, hintStyle: TextStyle( color: Color(0xff999999), fontSize: 14.sp, ), contentPadding: const EdgeInsets.symmetric( horizontal: 15, ), counterText: '', isDense: true, filled: false, focusColor: Colors.transparent, hoverColor: Colors.transparent, border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, ), style: TextStyle( fontSize: ScreenUtil().setSp(14), color: Colors.white, ), ), ), //原来的emoji按钮 //SizedBox(width: 10.w,), ], ), ), ), SizedBox(width: 10.w), !showEmoji ? GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { SCKeybordUtil.hide(context); setState(() { if (showTools) { showTools = false; } showEmoji = !showEmoji; }); }, child: Image.asset( _strategy.getSCMessageChatPageEmojiIcon(), width: 24.w, color: Colors.white, fit: BoxFit.fill, ), ) : Container(), showEmoji ? GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { SCKeybordUtil.hide(context); setState(() { if (showTools) { showTools = false; } showEmoji = !showEmoji; }); }, child: Image.asset( _strategy.getSCMessageChatPageChatKeyboardIcon(), width: 24.w, color: Colors.white, fit: BoxFit.fill, ), ) : Container(), if (!showSend) SizedBox(width: 12.w), if (showSend) SizedBox(width: 5.w), Visibility( visible: !showSend, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { SCKeybordUtil.hide(context); setState(() { if (showEmoji) { showEmoji = false; } showTools = !showTools; }); }, child: Image.asset( _strategy.getSCMessageChatPageAddIcon(), width: 24.w, color: Colors.white, fit: BoxFit.fill, ), ), ), Visibility( visible: showSend, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () async { if (_textController.text.isNotEmpty) { SCKeybordUtil.hide(context); sendMsg(_textController.text); showSend = false; setState(() {}); _textController.clear(); } }, child: Image.asset( _strategy.getSCMessageChatPageSendMessageIcon(), height: 22.w, color: Colors.white, width: 32.w, ), ), ), SizedBox(width: 15.w), ], ), ), ); } ///发消息 sendMsg(String msg) async { if (!canSendMsg) { SCTts.show(SCAppLocalizations.of(context)!.canSendMsgTips); return; } Provider.of( context, listen: false, ).sendC2CTextMsg(msg, currentConversation!); } ///发消息 sendCustomMsg(String msg, String extension) async { if (!canSendMsg) { SCTts.show(SCAppLocalizations.of(context)!.canSendMsgTips); return; } Provider.of( context, listen: false, ).sendC2CCustomMsg(msg, currentConversation!, extension); } ///工具栏 Widget _tools(RtmProvider provider) { return Visibility( visible: showTools, child: Container( color: Colors.white, padding: EdgeInsets.only(bottom: 18.w, top: 5.w), child: Row( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ SizedBox(width: 20.w), Expanded( child: _toolsItem( _strategy.getSCMessageChatPageCameraIcon(), SCAppLocalizations.of(context)!.camera, () async { if (!canSendMsg) { SCTts.show(SCAppLocalizations.of(context)!.canSendMsgTips); return; } var pick = await ImagePick.pickFromCamera(context); if (pick != null) { provider.sendImageMsg( file: pick, conversation: currentConversation!, ); } }, ), ), Expanded( child: _toolsItem( _strategy.getSCMessageChatPagePictureIcon(), SCAppLocalizations.of(context)!.album, () async { if (!canSendMsg) { SCTts.show(SCAppLocalizations.of(context)!.canSendMsgTips); return; } final List? result = await ImagePick.pickPicAndVideoFromGallery(context); if (result == null) return; // 用户取消选择 if (result.isNotEmpty) { provider.sendImageMsg( selectedList: result, conversation: currentConversation!, ); } }, ), ), Expanded(flex: 1, child: SizedBox()), Expanded(flex: 1, child: SizedBox()), ], ), ), ); } _emoji() { return Visibility( visible: showEmoji, child: Container( color: Colors.white, height: 150.w, child: GridView.builder( padding: EdgeInsets.symmetric(horizontal: 15.w), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 8, crossAxisSpacing: 8.w, mainAxisSpacing: 8.w, ), itemBuilder: (c, index) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { var text = SCEmojiDatas.smileys[index]; final TextEditingValue value = _textController.value; _textController.value = value.copyWith( text: value.text + text, selection: TextSelection.fromPosition( TextPosition(offset: text.length), ), ); showSend = true; setState(() {}); }, child: Padding( padding: EdgeInsets.all(4.0.w), child: ExtendedText( SCEmojiDatas.smileys[index], // specialTextSpanBuilder: MySpecialTextSpanBuilder(), style: TextStyle(fontSize: sp(15)), ), ), ); }, itemCount: SCEmojiDatas.smileys.length, ), ), ); } ///工具项 _toolsItem(String image, String title, Function() onclick) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onclick, child: Column( children: [ Image.asset(image, width: 50.w, fit: BoxFit.fitWidth), SizedBox(height: 9.w), Text(title, style: TextStyle(fontSize: 11.sp, color: Colors.black)), ], ), ); } _onRevokeMessage(String msgId) { final index = currentConversationMessageList.indexWhere( (msg) => msgId == msg.msgID, ); if (index < 0) { return; } currentConversationMessageList.removeAt(index); setState(() {}); } _onMessageRecvC2CRead(List messageIDList) { for (var msg in currentConversationMessageList) { for (var id in messageIDList) { if (id == msg.msgID) { msg.isPeerRead = true; } } } setState(() {}); } _onNewMessage(V2TimMessage? message, {String? msgId}) async { if (message == null) { return; } if (_isGroupConversation) { if (message.groupID != currentConversation?.groupID) return; } else if (message.userID != currentConversation?.userID) { return; } int? index; for (var element in currentConversationMessageList) { if (msgId != null) { if (msgId == element.msgID) { element.status = MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL; setState(() {}); continue; } } else { if (element.msgID == message.msgID) { index = currentConversationMessageList.indexOf(element); continue; } } } if (msgId != null) { return; } if (index != null) { currentConversationMessageList[index] = message; } else { _appendOrReplaceMessage(message); } await _markCurrentConversationAsRead(); //发送已读回执 await TencentImSDKPlugin.v2TIMManager .getMessageManager() .sendMessageReadReceipts(messageIDList: [message.msgID!]); setState(() {}); _scrollToLatestMessage(); // await FTIM.getContactManager().setReadMessage(currentConversation); } } class _MessageItem extends StatelessWidget { static bool _isOpeningCpInviteDialog = false; final V2TimMessage message; final V2TimMessage? preMessage; final SCVipResourceRes? selfVipChatBubble; final V2TimConversation? currentConversation; BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; // final FTIMUserProfile userProfile; final SocialChatUserProfile? friend; bool isSystem = false; List currentConversationMessageList = []; final Map localCpInviteStates; final void Function(String applyId, _CpInviteMessageState state)? onCpInviteStateChanged; Function updateCall; ///上一条 late BuildContext context; _MessageItem({ required this.message, this.selfVipChatBubble, this.currentConversation, this.preMessage, // this.userProfile, this.isSystem = false, required this.currentConversationMessageList, this.localCpInviteStates = const {}, this.onCpInviteStateChanged, this.friend, required this.updateCall, }); @override Widget build(BuildContext context) { this.context = context; SocialChatUserProfile? me = AccountStorage().getCurrentUser()?.userProfile; bool showTime = true; int timestamp = (message.timestamp ?? 0) * 1000; int preTimestamp = (preMessage?.timestamp ?? 0) * 1000; if (preMessage != null) { ///5分钟以内 不显示 if (DateTime.fromMillisecondsSinceEpoch(timestamp) .difference(DateTime.fromMillisecondsSinceEpoch(preTimestamp)) .inMilliseconds < 1000 * 60 * 5) { showTime = false; } } String time = SCMDateUtils.formatMessageTime( context, DateTime.fromMillisecondsSinceEpoch(timestamp), ); if (message.status == MessageStatus.V2TIM_MSG_STATUS_LOCAL_REVOKED) { return Container( alignment: AlignmentDirectional.center, padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w), child: text( SCAppLocalizations.of(context)!.messageHasBeenRecalled, fontSize: 12.sp, textColor: Colors.white54, fontWeight: FontWeight.w400, ), ); } final customData = _customDataFromMessage(message); if (customData != null && _isDuplicateCpInviteMessageForConversation(customData)) { return const SizedBox.shrink(); } final cpRelationFormedCard = _cpRelationFormedMessageCard(); final cpStatusLineText = _cpStatusLineTextForMessage(); return Container( margin: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w), child: Column( crossAxisAlignment: !message.isSelf! ? CrossAxisAlignment.start : CrossAxisAlignment.end, // textDirection: message.isSelf ? TextDirection.rtl : TextDirection.ltr, children: [ Visibility( visible: showTime, child: Container( margin: EdgeInsets.only(bottom: 12.w), child: Row( children: [ Spacer(), Container( alignment: Alignment.center, padding: EdgeInsets.symmetric( horizontal: width(15), vertical: 8.w, ), decoration: BoxDecoration( //color: Color(0xFFEAEFF7), borderRadius: BorderRadius.all( Radius.circular(height(13)), ), ), child: Text( time, style: TextStyle( fontSize: sp(12), color: Colors.white54, fontWeight: FontWeight.w400, decoration: TextDecoration.none, height: 1, ), ), ), Spacer(), ], ), ), ), if (cpStatusLineText != null) _buildCpStatusLine(cpStatusLineText), if (cpRelationFormedCard != null) Padding( padding: EdgeInsets.only( top: cpStatusLineText == null ? 0 : 10.w, ), child: cpRelationFormedCard, ) else if (cpStatusLineText == null) Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, textDirection: message.isSelf! ? TextDirection.rtl : TextDirection.ltr, children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { if (isSystem) { return; } SCNavigatorUtils.push( context, replace: AccountStorage().getCurrentUser()?.userProfile?.id != message.sender, "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == message.sender}&tageId=${message.sender}", ); }, child: message.sender == "administrator" || message.sender == "customer" ? ExtendedImage.asset( _strategy.getSCMessageChatPageSystemHeadImage(), width: 45.w, shape: BoxShape.circle, fit: BoxFit.cover, ) : netImage( url: message.isSelf! ? (me?.userAvatar ?? "") : (friend?.userAvatar ?? ""), shape: BoxShape.circle, width: width(45), ), ), // 他人消息的阴影间隔是10dp Container(width: 8.w), Row( textDirection: message.isSelf! ? TextDirection.rtl : TextDirection.ltr, children: [ Container( alignment: message.isSelf! ? AlignmentDirectional.topEnd : AlignmentDirectional.topStart, margin: EdgeInsets.only(top: 4.w), child: _msg(), ), SizedBox(width: 6.w), Visibility( visible: message.status == MessageStatus.V2TIM_MSG_STATUS_SENDING, child: CupertinoActivityIndicator(), ), Visibility( visible: message.status == MessageStatus.V2TIM_MSG_STATUS_SEND_FAIL, child: Icon(Icons.error, color: Colors.red), ), ], ), Spacer(), ], ), ], ), ); } Widget _buildCpStatusLine(String text) { return SizedBox( width: double.infinity, child: Center( child: Text( text, textAlign: TextAlign.center, style: TextStyle( fontSize: sp(14), height: 1.2, color: const Color(0xff999999), fontWeight: FontWeight.w700, decoration: TextDecoration.none, ), ), ), ); } Widget _buildCpBuildMessage(Map data) { final cpInvite = _cpInviteFromMessageData(data); final state = _cpInviteStateForMessage(data); final actionState = _cpInviteActionState(state); final subtitle = _cpInviteMessageSubtitle(state); final giftCover = cpInvite?.giftCover?.trim() ?? ""; return Builder( builder: (ct) { return GestureDetector( onLongPress: () => _showMsgItemMenu(ct, ""), onTap: () => _openCpInviteDialogFromMessage(data, cpInvite, actionState), child: Container( width: 251.w, height: 64.w, decoration: const BoxDecoration(color: Color(0xff071D1A)), child: Stack( children: [ Positioned( left: 0, top: 6.w, child: Image.asset( 'sc_images/room/cp_invite/sc_cp_system_notice_bg.png', width: 251.w, height: 53.w, fit: BoxFit.fill, ), ), Positioned( left: 14.w, top: 15.w, child: _buildCpInviteGiftThumb(giftCover), ), Positioned( left: 75.w, top: 18.w, child: SizedBox( width: 160.w, child: Text( SCAppLocalizations.of( context, )!.closeFriendInvitationTitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: const Color(0xffFF2A70), fontSize: 10.sp, fontWeight: FontWeight.w500, height: 1, decoration: TextDecoration.none, ), ), ), ), Positioned( left: 75.w, top: 35.w, child: SizedBox( width: 160.w, child: Text( subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: const Color(0xff67576F), fontSize: 8.sp, fontWeight: FontWeight.w400, height: 1, decoration: TextDecoration.none, ), ), ), ), ], ), ), ); }, ); } void _openCpInviteDialogFromMessage( Map data, SCSystemInvitMessageRes? cpInvite, RoomCpInviteActionState actionState, ) { unawaited(_openCpInviteDialogFromMessageAsync(data, cpInvite, actionState)); } Future _openCpInviteDialogFromMessageAsync( Map data, SCSystemInvitMessageRes? cpInvite, RoomCpInviteActionState actionState, ) async { if (_isOpeningCpInviteDialog || SmartDialog.checkExist(tag: RoomCpInviteDialog.dialogTag)) { return; } _isOpeningCpInviteDialog = true; try { var latestState = _cpInviteStateForMessage(data); if (actionState != RoomCpInviteActionState.pending || latestState != _CpInviteMessageState.pending) { _showCpInviteDialogFromMessage( data, cpInvite, actionState == RoomCpInviteActionState.pending ? _cpInviteActionState(latestState) : actionState, ); return; } latestState = await _latestCpInviteStateForMessage(data); if (!context.mounted) { return; } if (latestState != _CpInviteMessageState.pending) { _rememberCpInviteState(data, latestState); updateCall(); } _showCpInviteDialogFromMessage( data, cpInvite, _cpInviteActionState(latestState), ); } finally { _isOpeningCpInviteDialog = false; } } Widget _buildCpInviteGiftThumb(String giftCover) { final child = giftCover.isNotEmpty ? netImage( url: giftCover, width: 49.w, height: 49.w, fit: BoxFit.cover, defaultImg: 'sc_images/room/cp_profile/sc_cp_profile_gift_fallback.png', ) : Image.asset( 'sc_images/room/cp_profile/sc_cp_profile_gift_fallback.png', width: 49.w, height: 49.w, fit: BoxFit.cover, ); return ClipRRect(borderRadius: BorderRadius.circular(6.w), child: child); } Widget? _cpRelationFormedMessageCard() { final data = _customDataFromMessage(message); if (data == null || !_isCpInvitePayload(data)) { return null; } if (_cpInviteStateForMessage(data) != _CpInviteMessageState.accepted) { return null; } final cpInvite = _cpInviteFromMessageData(data); final content = _cpContentMap(data); final currentProfile = AccountStorage().getCurrentUser()?.userProfile; final currentName = _cpDisplayName( nickname: currentProfile?.userNickname, account: currentProfile?.account, fallback: currentProfile?.id, ); final inviterName = _cpDisplayName( nickname: _firstNonBlankString([ cpInvite?.userNickname, data["applyNickname"]?.toString(), data["applyUserNickname"]?.toString(), data["userNickname"]?.toString(), data["senderNickname"]?.toString(), content["applyNickname"]?.toString(), content["applyUserNickname"]?.toString(), content["userNickname"]?.toString(), content["senderNickname"]?.toString(), ], fallback: ""), account: _firstNonBlankString([ cpInvite?.account, data["account"]?.toString(), data["applyUserAccount"]?.toString(), content["account"]?.toString(), content["applyUserAccount"]?.toString(), ], fallback: ""), actualAccount: _firstNonBlankString([ cpInvite?.actualAccount, data["actualAccount"]?.toString(), data["applyUserId"]?.toString(), content["actualAccount"]?.toString(), content["applyUserId"]?.toString(), ], fallback: ""), fallback: message.sender, ); final inviterAvatar = _firstNonBlankString([ cpInvite?.userAvatar, data["applyUserAvatar"]?.toString(), data["userAvatar"]?.toString(), data["senderAvatar"]?.toString(), content["applyUserAvatar"]?.toString(), content["userAvatar"]?.toString(), content["senderAvatar"]?.toString(), ], fallback: ""); final receiverName = _cpDisplayName( nickname: _firstNonBlankString([ data["acceptNickname"]?.toString(), data["acceptUserNickname"]?.toString(), data["receiverNickname"]?.toString(), data["receiverUserNickname"]?.toString(), content["acceptNickname"]?.toString(), content["acceptUserNickname"]?.toString(), content["receiverNickname"]?.toString(), content["receiverUserNickname"]?.toString(), friend?.userNickname, currentConversation?.showName, ], fallback: ""), account: _firstNonBlankString([ data["acceptAccount"]?.toString(), data["receiverAccount"]?.toString(), content["acceptAccount"]?.toString(), content["receiverAccount"]?.toString(), friend?.account, ], fallback: ""), actualAccount: _firstNonBlankString([ data["acceptActualAccount"]?.toString(), data["acceptUserId"]?.toString(), data["receiverActualAccount"]?.toString(), data["receiverUserId"]?.toString(), content["acceptActualAccount"]?.toString(), content["acceptUserId"]?.toString(), content["receiverActualAccount"]?.toString(), content["receiverUserId"]?.toString(), friend?.id, currentConversation?.userID, ], fallback: ""), fallback: currentConversation?.userID ?? "User", ); final receiverAvatar = _firstNonBlankString([ data["acceptUserAvatar"]?.toString(), data["receiverAvatar"]?.toString(), data["receiverUserAvatar"]?.toString(), content["acceptUserAvatar"]?.toString(), content["receiverAvatar"]?.toString(), content["receiverUserAvatar"]?.toString(), friend?.userAvatar, currentConversation?.faceUrl, ], fallback: ""); final isInviter = _isCurrentUserCpInviter(data); final relationType = scNormalizeCpRelationType( _firstNonBlankString([ cpInvite?.relationType, data["relationType"]?.toString(), data["relation_type"]?.toString(), content["relationType"]?.toString(), content["relation_type"]?.toString(), ], fallback: "CP"), ); return Builder( builder: (ct) { return GestureDetector( onLongPress: () => _showMsgItemMenu(ct, ""), child: SizedBox( width: double.infinity, child: Center( child: RoomCpRelationFormedCard( leftName: isInviter ? currentName : inviterName, rightName: isInviter ? receiverName : currentName, leftAvatar: isInviter ? _firstNonBlankString([ currentProfile?.userAvatar, inviterAvatar, ]) : inviterAvatar, rightAvatar: isInviter ? receiverAvatar : _firstNonBlankString([ currentProfile?.userAvatar, receiverAvatar, ]), relationType: relationType, ), ), ), ); }, ); } String? _cpStatusLineTextForMessage() { final data = _customDataFromMessage(message); if (data == null || !_isCpInvitePayload(data)) { return null; } final state = _cpInviteStateForMessage(data); if (state == _CpInviteMessageState.pending) { return null; } final localizations = SCAppLocalizations.of(context)!; final relationType = _cpRelationTypeFromMessageData(data); if (state == _CpInviteMessageState.accepted) { return _cpAcceptedStatusLineText(relationType); } if (state == _CpInviteMessageState.expired) { if (relationType != "CP") { return "The ${scCpRelationPossessiveLabel(relationType)} invitation timed out."; } return localizations.closeFriendInviteTimedOut; } if (relationType == "CP") { return _isCurrentUserCpInviter(data) ? localizations.otherDeclinedCloseFriendInvite : localizations.youDeclinedCloseFriendInvite; } final relationLabel = scCpRelationPossessiveLabel(relationType); return _isCurrentUserCpInviter(data) ? "The other party declined your $relationLabel invitation." : "You declined the other party's $relationLabel invitation."; } String _cpAcceptedStatusLineText(String relationType) { final localizations = SCAppLocalizations.of(context)!; switch (scNormalizeCpRelationType(relationType)) { case "BROTHER": return "Have become sworn brothers"; case "SISTERS": return "Have become sisters"; case "CP": default: return localizations.closeFriendsHaveBecome; } } void _showCpInviteDialogFromMessage( Map data, SCSystemInvitMessageRes? cpInvite, RoomCpInviteActionState actionState, ) { final effectiveActionState = actionState == RoomCpInviteActionState.pending ? _cpInviteActionState(_cpInviteStateForMessage(data)) : actionState; final currentProfile = AccountStorage().getCurrentUser()?.userProfile; final currentName = _cpDisplayName( nickname: currentProfile?.userNickname, account: currentProfile?.account, fallback: currentProfile?.id, ); final inviterName = _cpDisplayName( nickname: cpInvite?.userNickname, account: cpInvite?.account, actualAccount: cpInvite?.actualAccount, ); final isInviter = _isCurrentUserCpInviter(data); final content = _cpContentMap(data); final receiverAvatar = _firstNonBlankString([ data["acceptUserAvatar"]?.toString(), data["receiverAvatar"]?.toString(), data["receiverUserAvatar"]?.toString(), content["acceptUserAvatar"]?.toString(), content["receiverAvatar"]?.toString(), content["receiverUserAvatar"]?.toString(), friend?.userAvatar, currentConversation?.faceUrl, ]); final receiverHeaddress = _firstNonBlankString([ data["acceptHeaddress"]?.toString(), data["acceptAvatarFrame"]?.toString(), data["receiverHeaddress"]?.toString(), data["receiverAvatarFrame"]?.toString(), content["acceptHeaddress"]?.toString(), content["acceptAvatarFrame"]?.toString(), content["receiverHeaddress"]?.toString(), content["receiverAvatarFrame"]?.toString(), friend?.getHeaddress()?.sourceUrl, ]); final receiverHeaddressCover = _firstNonBlankString([ data["acceptHeaddressCover"]?.toString(), data["acceptAvatarFrameCover"]?.toString(), data["receiverHeaddressCover"]?.toString(), data["receiverAvatarFrameCover"]?.toString(), content["acceptHeaddressCover"]?.toString(), content["acceptAvatarFrameCover"]?.toString(), content["receiverHeaddressCover"]?.toString(), content["receiverAvatarFrameCover"]?.toString(), friend?.getHeaddress()?.cover, ]); final receiverName = isInviter ? _cpDisplayName( nickname: _firstNonBlankString([ data["acceptNickname"]?.toString(), data["acceptUserNickname"]?.toString(), data["receiverNickname"]?.toString(), data["receiverUserNickname"]?.toString(), content["acceptNickname"]?.toString(), content["acceptUserNickname"]?.toString(), content["receiverNickname"]?.toString(), content["receiverUserNickname"]?.toString(), friend?.userNickname, currentConversation?.showName, ], fallback: ""), account: _firstNonBlankString([ data["acceptAccount"]?.toString(), data["receiverAccount"]?.toString(), content["acceptAccount"]?.toString(), content["receiverAccount"]?.toString(), friend?.account, ], fallback: ""), actualAccount: _firstNonBlankString([ data["acceptActualAccount"]?.toString(), content["acceptActualAccount"]?.toString(), data["acceptUserId"]?.toString(), content["acceptUserId"]?.toString(), data["receiverActualAccount"]?.toString(), data["receiverUserId"]?.toString(), content["receiverActualAccount"]?.toString(), content["receiverUserId"]?.toString(), friend?.id, currentConversation?.userID, ], fallback: ""), fallback: currentConversation?.userID ?? "User", ) : currentName; final relationType = scNormalizeCpRelationType(cpInvite?.relationType); final applyId = _cpApplyIdFromData(data); RoomCpInviteDialog.show( context, inviter: RoomCpInviteDialogUser( name: inviterName, avatarUrl: cpInvite?.userAvatar ?? "", headdress: isInviter ? currentProfile?.getHeaddress()?.sourceUrl ?? "" : _firstNonBlankString([ cpInvite?.userHeaddress, data["userHeaddress"]?.toString(), data["userAvatarFrame"]?.toString(), content["userHeaddress"]?.toString(), content["userAvatarFrame"]?.toString(), ], fallback: ""), headdressCover: isInviter ? currentProfile?.getHeaddress()?.cover ?? "" : _firstNonBlankString([ cpInvite?.userHeaddressCover, data["userHeaddressCover"]?.toString(), data["userAvatarFrameCover"]?.toString(), content["userHeaddressCover"]?.toString(), content["userAvatarFrameCover"]?.toString(), ], fallback: ""), ), receiver: RoomCpInviteDialogUser( name: receiverName, avatarUrl: isInviter ? receiverAvatar : _firstNonBlankString([ currentProfile?.userAvatar, receiverAvatar, ]), headdress: isInviter ? receiverHeaddress : _firstNonBlankString([ currentProfile?.getHeaddress()?.sourceUrl, receiverHeaddress, ]), headdressCover: isInviter ? receiverHeaddressCover : _firstNonBlankString([ currentProfile?.getHeaddress()?.cover, receiverHeaddressCover, ]), ), style: isInviter ? RoomCpInviteDialogStyle.waiting : RoomCpInviteDialogStyle.incoming, actionState: effectiveActionState, countdownSeconds: _cpInviteSecondsLeft(data), descriptionText: _cpInviteDescriptionText(relationType), onAccept: !isInviter && cpInvite != null && effectiveActionState == RoomCpInviteActionState.pending && applyId.isNotEmpty ? () => _processCpInviteFromMessage(applyId, cpInvite, true, data) : null, onReject: !isInviter && cpInvite != null && effectiveActionState == RoomCpInviteActionState.pending && applyId.isNotEmpty ? () => _processCpInviteFromMessage(applyId, cpInvite, false, data) : null, ); } void _processCpInviteFromMessage( String applyId, SCSystemInvitMessageRes invite, bool agree, Map data, ) { unawaited(_processCpInviteFromMessageAsync(applyId, invite, agree, data)); } Future _processCpInviteFromMessageAsync( String applyId, SCSystemInvitMessageRes invite, bool agree, Map data, ) async { _cpMessageLog('process applyId=$applyId agree=$agree'); final latestState = await _latestCpInviteStateForMessage(data); if (latestState != _CpInviteMessageState.pending) { _cpMessageLog( 'skip process applyId=$applyId agree=$agree latestState=$latestState', ); if (!context.mounted) { return; } _showCpInviteDialogFromMessage( data, invite, _cpInviteActionState(latestState), ); updateCall(); return; } if (!context.mounted) { return; } final profileManager = Provider.of( context, listen: false, ); final success = await profileManager.cpRlationshipProcessApply( context, applyId, agree, ); if (!success) { _cpMessageLog('process failed applyId=$applyId agree=$agree'); return; } if (!context.mounted) { return; } SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag); onCpInviteStateChanged?.call( applyId, agree ? _CpInviteMessageState.accepted : _CpInviteMessageState.rejected, ); _cpMessageLog( 'process submitted applyId=$applyId agree=$agree ' 'wait_backend_result=true', ); unawaited(profileManager.refreshLoadedCpProfiles()); updateCall(); } Future<_CpInviteMessageState> _latestCpInviteStateForMessage( Map data, ) async { final localState = _cpInviteStateForMessage(data); if (localState != _CpInviteMessageState.pending) { return localState; } final applyId = _cpApplyIdFromData(data); if (applyId.isEmpty) { return localState; } try { final status = await SCAccountRepository().cpRelationshipApplyStatus( applyId, ); final backendState = _cpInviteStateFromBackendStatus(status); if (backendState != null) { return backendState; } } catch (error) { _cpMessageLog('query apply status failed applyId=$applyId error=$error'); } return _cpInviteSecondsLeft(data) <= 0 ? _CpInviteMessageState.expired : localState; } void _rememberCpInviteState( Map data, _CpInviteMessageState state, ) { if (state == _CpInviteMessageState.pending) { return; } final applyId = _cpApplyIdFromData(data); if (applyId.isEmpty) { return; } onCpInviteStateChanged?.call(applyId, state); } _CpInviteMessageState? _cpInviteStateFromBackendStatus(String? status) { final text = status?.trim().toUpperCase() ?? ""; if (text.isEmpty || text == "WAIT" || text == "WAITING") { return text.isEmpty ? null : _CpInviteMessageState.pending; } if (text.contains("EXPIRE") || text.contains("TIMEOUT") || text.contains("OVERDUE")) { return _CpInviteMessageState.expired; } if (text.contains("REFUSE") || text.contains("REJECT") || text.contains("DECLIN") || text.contains("DENY")) { return _CpInviteMessageState.rejected; } if (text.contains("AGREE") || text.contains("ACCEPT") || text.contains("APPROVE") || text.contains("ESTABLISH")) { return _CpInviteMessageState.accepted; } return null; } Map? _customDataFromMessage(V2TimMessage msg) { if (msg.elemType != MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) { return null; } final raw = msg.customElem?.data; if (raw == null || raw.trim().isEmpty) { return null; } try { final decoded = jsonDecode(raw); if (decoded is Map) { return decoded.map((key, value) => MapEntry(key.toString(), value)); } } catch (_) {} return null; } SCSystemInvitMessageRes? _cpInviteFromMessageData(dynamic data) { try { return SCSystemInvitMessageRes.fromMessageData(data); } catch (error) { _cpMessageLog('parse invite failed error=$error'); } return null; } _CpInviteMessageState _cpInviteStateForMessage(Map data) { final localState = _localCpInviteStateForData(data); if (localState != null) { return localState; } final explicitState = _cpInviteStateFromConversation(data); if (explicitState != _CpInviteMessageState.pending) { return explicitState; } return _cpInviteSecondsLeft(data) <= 0 ? _CpInviteMessageState.expired : _CpInviteMessageState.pending; } _CpInviteMessageState _cpInviteStateFromConversation( Map currentData, ) { final applyId = _cpApplyIdFromData(currentData); final localState = _localCpInviteStateForApplyId(applyId); if (localState != null) { return localState; } final currentState = _cpInviteStateFromData(currentData); if (currentState != _CpInviteMessageState.pending || applyId.isEmpty) { return currentState; } final currentTimestamp = message.timestamp ?? 0; for (final item in currentConversationMessageList) { if ((item.timestamp ?? 0) < currentTimestamp) { continue; } final data = _customDataFromMessage(item); if (data == null || _cpApplyIdFromData(data) != applyId) { continue; } final state = _cpInviteStateFromData(data); if (state != _CpInviteMessageState.pending) { return state; } } return _CpInviteMessageState.pending; } _CpInviteMessageState? _localCpInviteStateForData(Map data) { return _localCpInviteStateForApplyId(_cpApplyIdFromData(data)); } _CpInviteMessageState? _localCpInviteStateForApplyId(String applyId) { final normalizedApplyId = applyId.trim(); if (normalizedApplyId.isEmpty) { return null; } return localCpInviteStates[normalizedApplyId]; } bool _isDuplicateCpInviteMessageForConversation(Map data) { final currentKey = _cpInviteMessageDedupKey(data); if (currentKey.isEmpty) { return false; } final currentIndex = currentConversationMessageList.indexWhere( _isCurrentConversationMessage, ); if (currentIndex <= 0) { return false; } for (var i = 0; i < currentIndex; i++) { final otherData = _customDataFromMessage( currentConversationMessageList[i], ); if (otherData == null) { continue; } if (_cpInviteMessageDedupKey(otherData) == currentKey) { return true; } } return false; } bool _isCurrentConversationMessage(V2TimMessage item) { final currentMsgId = message.msgID?.trim() ?? ""; final itemMsgId = item.msgID?.trim() ?? ""; if (currentMsgId.isNotEmpty && itemMsgId.isNotEmpty) { return currentMsgId == itemMsgId; } final currentSeq = message.seq?.trim() ?? ""; final itemSeq = item.seq?.trim() ?? ""; if (currentSeq.isNotEmpty && itemSeq.isNotEmpty) { return currentSeq == itemSeq && item.timestamp == message.timestamp; } return identical(item, message); } String _cpInviteMessageDedupKey(Map data) { if (!_isCpInvitePayload(data)) { return ""; } final state = _cpInviteStateForMessage(data); final applyId = _cpApplyIdFromData(data); if (applyId.isEmpty) { return ""; } return "$applyId|${state.name}"; } _CpInviteMessageState _cpInviteStateFromData(Map data) { final content = _cpContentMap(data); final values = [ data["noticeType"]?.toString() ?? "", data["type"]?.toString() ?? "", data["noticeAction"]?.toString() ?? "", data["action"]?.toString() ?? "", data["status"]?.toString() ?? "", data["applyStatus"]?.toString() ?? "", data["processStatus"]?.toString() ?? "", data["result"]?.toString() ?? "", data["state"]?.toString() ?? "", content["noticeType"]?.toString() ?? "", content["type"]?.toString() ?? "", content["noticeAction"]?.toString() ?? "", content["action"]?.toString() ?? "", content["status"]?.toString() ?? "", content["applyStatus"]?.toString() ?? "", content["processStatus"]?.toString() ?? "", content["result"]?.toString() ?? "", content["state"]?.toString() ?? "", ].map((value) => value.trim().toUpperCase()).join("|"); final expiredValue = _cpInviteBoolValue([ data["expired"], data["isExpired"], content["expired"], content["isExpired"], ]); if (expiredValue == true) { return _CpInviteMessageState.expired; } final agreeValue = _cpInviteBoolValue([ data["agree"], data["accepted"], data["isAgree"], content["agree"], content["accepted"], content["isAgree"], ]); if (agreeValue == true) { return _CpInviteMessageState.accepted; } if (agreeValue == false) { return _CpInviteMessageState.rejected; } if (values.contains("TIMEOUT") || values.contains("EXPIRE")) { return _CpInviteMessageState.expired; } if (values.contains("REJECT") || values.contains("DECLIN") || values.contains("REFUSE") || values.contains("DENY")) { return _CpInviteMessageState.rejected; } if (values.contains("ACCEPT") || values.contains("AGREE") || values.contains("APPROVE") || values.contains("ESTABLISH")) { return _CpInviteMessageState.accepted; } return _CpInviteMessageState.pending; } bool? _cpInviteBoolValue(Iterable values) { for (final value in values) { if (value is bool) { return value; } final text = value?.toString().trim().toLowerCase() ?? ""; if (text.isEmpty) { continue; } if (text == "true" || text == "1" || text == "yes" || text == "agree") { return true; } if (text == "false" || text == "0" || text == "no" || text == "reject" || text == "decline" || text == "refuse") { return false; } } return null; } bool _isCpInvitePayload(Map data) { final content = _cpContentMap(data); final noticeType = [ data["noticeType"]?.toString() ?? "", data["type"]?.toString() ?? "", content["noticeType"]?.toString() ?? "", content["type"]?.toString() ?? "", ].join("|").toUpperCase(); return noticeType.contains(SCSysytemMessageType.CP_BUILD.name); } bool _isCurrentUserCpInviter(Map data) { final currentProfile = AccountStorage().getCurrentUser()?.userProfile; if (currentProfile == null) { return false; } final currentIds = _cpCurrentProfileIds(currentProfile); if (currentIds.isEmpty) { return false; } final receiverIds = _cpInviteReceiverIdentityValues(data); if (currentIds.any(receiverIds.contains)) { return false; } final inviterIds = _cpInviteInviterIdentityValues(data); return currentIds.any(inviterIds.contains); } Set _cpCurrentProfileIds(SocialChatUserProfile currentProfile) { return [currentProfile.id, currentProfile.account] .map((value) => value?.trim() ?? "") .where((value) => value.isNotEmpty) .toSet(); } Set _cpInviteInviterIdentityValues(Map data) { final content = _cpContentMap(data); final values = [ data["account"]?.toString(), data["actualAccount"]?.toString(), data["applyUserId"]?.toString(), data["applyUserAccount"]?.toString(), data["applyActualAccount"]?.toString(), data["applyUserActualAccount"]?.toString(), data["inviterActualAccount"]?.toString(), data["inviterUserId"]?.toString(), data["inviterId"]?.toString(), data["senderUserId"]?.toString(), data["senderId"]?.toString(), data["fromUserId"]?.toString(), content["applyUserId"]?.toString(), content["applyUserAccount"]?.toString(), content["applyActualAccount"]?.toString(), content["applyUserActualAccount"]?.toString(), content["inviterActualAccount"]?.toString(), content["inviterUserId"]?.toString(), content["inviterId"]?.toString(), content["senderUserId"]?.toString(), content["senderId"]?.toString(), content["fromUserId"]?.toString(), ]; final explicitValues = values.map((value) => value?.trim() ?? "").where((value) { return value.isNotEmpty; }).toSet(); if (explicitValues.isNotEmpty) { return explicitValues; } return [ content["account"]?.toString(), content["actualAccount"]?.toString(), content["userId"]?.toString(), ].map((value) => value?.trim() ?? "").where((value) { return value.isNotEmpty; }).toSet(); } Set _cpInviteReceiverIdentityValues(Map data) { final content = _cpContentMap(data); return [ data["acceptUserId"]?.toString(), data["acceptAccount"]?.toString(), data["acceptActualAccount"]?.toString(), data["receiverId"]?.toString(), data["receiverUserId"]?.toString(), data["receiverAccount"]?.toString(), data["receiverActualAccount"]?.toString(), data["targetUserId"]?.toString(), data["toUserId"]?.toString(), content["acceptUserId"]?.toString(), content["acceptAccount"]?.toString(), content["acceptActualAccount"]?.toString(), content["receiverId"]?.toString(), content["receiverUserId"]?.toString(), content["receiverAccount"]?.toString(), content["receiverActualAccount"]?.toString(), content["targetUserId"]?.toString(), content["toUserId"]?.toString(), ].map((value) => value?.trim() ?? "").where((value) { return value.isNotEmpty; }).toSet(); } int _cpInviteSecondsLeft(Map data) { final content = _cpContentMap(data); final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; final explicitExpireAt = _firstEpochSeconds([ data["expireTime"], data["expiredTime"], data["expireAt"], data["expiredAt"], data["timeoutAt"], data["endTime"], data["dismissEndTime"], content["expireTime"], content["expiredTime"], content["expireAt"], content["expiredAt"], content["timeoutAt"], content["endTime"], content["dismissEndTime"], ]); if (explicitExpireAt != null) { return (explicitExpireAt - now).clamp(0, 86400).toInt(); } final startAt = _firstEpochSeconds([ data["createTime"], data["createdAt"], data["sendTime"], content["createTime"], content["createdAt"], content["sendTime"], message.timestamp, ]) ?? now; return (startAt + 86400 - now).clamp(0, 86400).toInt(); } int? _firstEpochSeconds(Iterable values) { for (final value in values) { final text = value?.toString().trim() ?? ""; if (text.isEmpty) { continue; } final parsed = num.tryParse(text); if (parsed == null || parsed <= 0) { continue; } return parsed > 9999999999 ? parsed ~/ 1000 : parsed.toInt(); } return null; } String _cpApplyIdFromData(Map data) { final content = _cpContentMap(data); return _firstNonBlankString([ data["expand"]?.toString(), data["applyId"]?.toString(), data["apply_id"]?.toString(), data["id"]?.toString(), content["expand"]?.toString(), content["applyId"]?.toString(), content["apply_id"]?.toString(), content["id"]?.toString(), ], fallback: ""); } String _cpRelationTypeFromMessageData(Map data) { final invite = _cpInviteFromMessageData(data); final content = _cpContentMap(data); return scNormalizeCpRelationType( _firstNonBlankString([ invite?.relationType, data["relationType"]?.toString(), data["relation_type"]?.toString(), content["relationType"]?.toString(), content["relation_type"]?.toString(), ], fallback: "CP"), ); } Map _cpContentMap(Map data) { final content = data["content"]; try { if (content is String && content.trim().isNotEmpty) { final decoded = jsonDecode(content); if (decoded is Map) { return decoded.map((key, value) => MapEntry(key.toString(), value)); } } if (content is Map) { return content.map((key, value) => MapEntry(key.toString(), value)); } } catch (_) {} return const {}; } RoomCpInviteActionState _cpInviteActionState(_CpInviteMessageState state) { switch (state) { case _CpInviteMessageState.expired: return RoomCpInviteActionState.expired; case _CpInviteMessageState.rejected: return RoomCpInviteActionState.rejected; case _CpInviteMessageState.accepted: return RoomCpInviteActionState.accepted; case _CpInviteMessageState.pending: return RoomCpInviteActionState.pending; } } String _cpInviteMessageSubtitle(_CpInviteMessageState state) { final localizations = SCAppLocalizations.of(context)!; switch (state) { case _CpInviteMessageState.expired: return localizations.closeFriendInviteTimedOut; case _CpInviteMessageState.rejected: return localizations.closeFriendRequestDeclined; case _CpInviteMessageState.accepted: return localizations.closeFriendsHaveBecome; case _CpInviteMessageState.pending: return localizations.closeFriendInvitationSubtitle; } } String _cpInviteDescriptionText(String relationType) { switch (scNormalizeCpRelationType(relationType)) { case "BROTHER": return 'Invite you to become Brothers'; case "SISTERS": return 'Invite you to become Sisters'; case "CP": default: return 'Invite you to become Couple'; } } String _cpDisplayName({ String? nickname, String? account, String? actualAccount, String? fallback, }) { for (final value in [nickname, actualAccount, account, fallback]) { final text = value?.trim() ?? ""; if (text.isNotEmpty) { return text; } } return "User"; } String _firstNonBlankString( Iterable values, { String fallback = "", }) { for (final value in values) { final text = value?.trim() ?? ""; if (text.isNotEmpty) { return text; } } return fallback; } void _cpMessageLog(String message) { debugPrint('[CP][Message] $message'); } _msg() { V2TIMElem? elem = message.textElem; if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT) { V2TimTextElem textElem = elem as V2TimTextElem; String content = textElem.text ?? ""; } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_IMAGE) { V2TimImageElem imageElem = message.imageElem as V2TimImageElem; var image; if (imageElem.path!.length > 2 && File("${imageElem.path}").existsSync()) { image = Image.file(File(imageElem.path!), fit: BoxFit.cover); } else { image = ExtendedImage.network( '${imageElem.imageList![1]?.url}', fit: BoxFit.cover, width: double.infinity, cache: true, shape: BoxShape.rectangle, borderRadius: BorderRadius.all(Radius.circular(width(6))), loadStateChanged: (ExtendedImageState state) { if (state.extendedImageLoadState == LoadState.completed) { return ExtendedRawImage( image: state.extendedImageInfo?.image, fit: BoxFit.cover, ); } else { return Image.asset( _strategy.getSCMessageChatPageLoadingIcon(), fit: BoxFit.cover, ); } }, ); } return Builder( builder: (ct) { return GestureDetector( onTap: () { List ims = []; int initialIndex = 0; int index = 0; for (var imageMsg in currentConversationMessageList) { if (imageMsg.elemType == MessageElemType.V2TIM_ELEM_TYPE_IMAGE) { String path = imageMsg.imageElem?.imageList![1]?.url ?? ""; if (path.isNotEmpty) { ims.add(imageMsg.imageElem?.imageList![1]?.url ?? ""); if (imageMsg.msgID == message.msgID) { initialIndex = index; } index = index + 1; } } } String encodedUrls = Uri.encodeComponent(jsonEncode(ims)); SCNavigatorUtils.push( context, "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=$initialIndex", ); }, onLongPress: () { _showMsgItemMenu(ct, ""); }, child: Container( constraints: BoxConstraints(maxWidth: 200.w), child: ClipRRect( borderRadius: BorderRadius.circular(6.w), child: image, ), ), ); }, ); } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_VIDEO) { V2TimVideoElem videoElem = message.videoElem as V2TimVideoElem; if (videoElem.snapshotPath != null && videoElem.snapshotPath!.isNotEmpty) { return Builder( builder: (ct) { return GestureDetector( child: Container( constraints: BoxConstraints(maxWidth: 200.w), child: ClipRRect( borderRadius: BorderRadius.circular(6.w), child: Stack( alignment: Alignment.center, children: [ Image.file( File(videoElem.snapshotPath!), fit: BoxFit.cover, ), Icon(Icons.play_circle, color: Colors.white, size: 55.w), ], ), ), ), onTap: () { String encodedVideoUrl = Uri.encodeComponent( videoElem.videoPath ?? "", ); SCNavigatorUtils.push( context, "${SCMainRoute.videoPlayer}?videoUrl=$encodedVideoUrl", ); }, onLongPress: () { _showMsgItemMenu(ct, ""); }, ); }, ); } else { return FutureBuilder>( // 假设 getMessageOnlineUrlForMessage 是对 TencentImSDKPlugin.v2TIMManager.getMessageManager().getMessageOnlineUrl 的封装,返回 Future future: SCMessageUtils().getMessageOnlineUrl( msgID: message.msgID ?? "", ), builder: (context, snapshot) { // 根据异步任务状态构建UI if (snapshot.connectionState == ConnectionState.waiting) { // 数据加载中,显示占位符 return Container(); } else if (snapshot.hasError) { // 数据加载失败,显示错误UI return Container(); } else if (snapshot.hasData) { // 数据加载成功,更新消息的mediaUrl并构建正常消息UI message.videoElem = snapshot.data?.data?.videoElem!; return Builder( builder: (ct) { return GestureDetector( child: Container( constraints: BoxConstraints(maxWidth: 200.w), child: ClipRRect( borderRadius: BorderRadius.circular(6.w), child: Stack( alignment: Alignment.center, children: [ SCPathUtils.getPathType( message.videoElem?.snapshotPath ?? "", ) == PathType.file ? Image.file( File(message.videoElem?.snapshotPath ?? ""), fit: BoxFit.cover, ) : netImage( url: message.videoElem?.snapshotUrl ?? "", ), Icon( Icons.play_circle, color: Colors.white, size: 55.w, ), ], ), ), ), onTap: () { String encodedVideoUrl = Uri.encodeComponent( message.videoElem?.videoUrl ?? "", ); SCNavigatorUtils.push( context, "${SCMainRoute.videoPlayer}?videoUrl=$encodedVideoUrl", ); }, onLongPress: () { _showMsgItemMenu(ct, ""); }, ); }, ); } else { // 其他情况 return Container(); } }, ); } } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) { final data = _customDataFromMessage(message); if (data != null && _isCpInvitePayload(data)) { return _buildCpBuildMessage(data); } } return _text(); } ///文本消息 _text() { String content = ""; if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_TEXT) { V2TimTextElem textElem = message.textElem!; content = textElem.text ?? ""; if (isSystem) {} } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_IMAGE) { content = SCAppLocalizations.of(context)!.image; } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_VIDEO) { content = SCAppLocalizations.of(context)!.video; } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_SOUND) { content = SCAppLocalizations.of(context)!.sound; } else if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) { V2TimCustomElem customElem = message.customElem!; content = customElem.data ?? ""; } return Builder( builder: (ct) { return GestureDetector( child: _buildTextBubble(content), onLongPress: () { _showMsgItemMenu(ct, content); }, ); }, ); } Widget _buildTextBubble(String content) { final chatBubble = (message.isSelf ?? false) ? _currentUserChatBubble() : _senderProfile()?.getChatBox(); final chatBubbleUrl = _resolveChatBubbleImageUrl(chatBubble); if (chatBubbleUrl.isNotEmpty) { return _buildVipTextBubble(content: content, imageUrl: chatBubbleUrl); } if (message.isSelf ?? false) { final vipChatBubbleUrl = _resolveVipChatBubbleImageUrl(selfVipChatBubble); if (vipChatBubbleUrl.isNotEmpty) { return _buildVipTextBubble( content: content, imageUrl: vipChatBubbleUrl, ); } } return _buildDefaultTextBubble(content); } PropsResources? _currentUserChatBubble() { final memoryBubble = AccountStorage().getCurrentUser()?.userProfile?.getChatBox(); if (_resolveChatBubbleImageUrl(memoryBubble).isNotEmpty) { return memoryBubble; } final userJson = DataPersistence.getCurrentUser(); if (userJson.isEmpty) { return memoryBubble; } try { return SocialChatLoginRes.fromJson( jsonDecode(userJson), ).userProfile?.getChatBox(); } catch (_) { return memoryBubble; } } SocialChatUserProfile? _senderProfile() { if (message.isSelf ?? false) { return AccountStorage().getCurrentUser()?.userProfile; } return friend; } Widget _buildDefaultTextBubble(String content) { return Container( decoration: BoxDecoration( color: Color(0xff18F2B1).withValues(alpha: 0.1), borderRadius: BorderRadius.only( topLeft: message.isSelf! ? Radius.circular(8) : Radius.circular(0), topRight: Radius.circular(8.w), bottomLeft: Radius.circular(8.w), bottomRight: message.isSelf! ? Radius.circular(0) : Radius.circular(8), ), ), padding: EdgeInsets.all(8.0.w), child: Container( constraints: BoxConstraints(maxWidth: width(220)), child: ExtendedText( content, style: TextStyle(fontSize: sp(14), color: Colors.white), ), ), ); } Widget _buildVipTextBubble({ required String content, required String imageUrl, }) { final textStyle = TextStyle(fontSize: sp(14), color: Colors.white); final multilineMaxBubbleWidth = math.min( ScreenUtil().screenWidth * 0.64, 260.w, ); final multilineMaxTextWidth = math.max( 82.w, multilineMaxBubbleWidth - 76.w, ); final multilineTextPainter = TextPainter( text: TextSpan(text: content, style: textStyle), textDirection: Directionality.of(context), )..layout(maxWidth: multilineMaxTextWidth); if (multilineTextPainter.computeLineMetrics().length > 1) { return _buildMultilineVipTextBubble( content: content, imageUrl: imageUrl, textStyle: textStyle, textSize: multilineTextPainter.size, maxBubbleWidth: multilineMaxBubbleWidth, ); } return _buildSingleLineVipTextBubble( content: content, imageUrl: imageUrl, textStyle: textStyle, ); } Widget _buildMultilineVipTextBubble({ required String content, required String imageUrl, required TextStyle textStyle, required Size textSize, required double maxBubbleWidth, }) { final leftPadding = 34.w; final rightPadding = 42.w; final topPadding = 16.w; final bottomPadding = 18.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: content, style: textStyle), textDirection: Directionality.of(context), )..layout(maxWidth: textMaxWidth); final lineMetrics = textPainter.computeLineMetrics(); final lineCount = math.max(1, lineMetrics.length); final lineHeight = lineMetrics.isEmpty ? textPainter.preferredLineHeight : lineMetrics.fold( 0, (height, line) => math.max(height, line.height), ); final lineSafetyPadding = lineHeight * math.max(0, lineCount - 1) * 0.55; final textBoxHeight = textPainter.size.height + lineSafetyPadding; final bubbleHeight = math.max( 76.w, textBoxHeight + topPadding + bottomPadding, ); return SizedBox( width: bubbleWidth, height: bubbleHeight, child: Stack( fit: StackFit.expand, children: [ _buildVipBubbleBackground( url: imageUrl, width: bubbleWidth, height: bubbleHeight, fallback: SizedBox(width: bubbleWidth, height: bubbleHeight), ), Padding( padding: EdgeInsetsDirectional.fromSTEB( leftPadding, topPadding, rightPadding, bottomPadding, ), child: Align( alignment: AlignmentDirectional.centerStart, child: SizedBox( width: textMaxWidth, height: textBoxHeight, child: ExtendedText(content, style: textStyle), ), ), ), ], ), ); } Widget _buildSingleLineVipTextBubble({ required String content, required String imageUrl, required TextStyle textStyle, }) { final leftPadding = 22.w; final rightPadding = 24.w; final topPadding = 12.w; final bottomPadding = 16.w; final minSkinWidth = 50.w; final minSkinHeight = 58.w; final textWidthSafety = 10.w; final maxTextWidth = width(220); final maxBubbleWidth = maxTextWidth + leftPadding + rightPadding; final textSize = _measureTextSize( text: content, 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, ); final skinAlignment = message.isSelf! ? AlignmentDirectional.centerEnd : AlignmentDirectional.centerStart; return SizedBox( width: bubbleWidth, height: bubbleHeight, 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: skinAlignment, ), ), Padding( padding: EdgeInsetsDirectional.fromSTEB( leftPadding, topPadding, rightPadding, bottomPadding, ), child: Align( alignment: AlignmentDirectional.centerStart, child: SizedBox( width: textMaxWidth, child: ExtendedText( content, style: textStyle, maxLines: 1, softWrap: false, ), ), ), ), ], ), ); } 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 _measureTextSize({ 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 _resolveChatBubbleImageUrl(PropsResources? resource) { final candidates = [ resource?.imSendCoverUrl, resource?.imOpenedUrl, resource?.imOpenedUrlTwo, resource?.imNotOpenedUrl, resource?.roomSendCoverUrl, resource?.roomOpenedUrl, resource?.roomNotOpenedUrl, resource?.cover, resource?.expand, resource?.sourceUrl, ]; return _firstNonBlank([ ...candidates.where((value) => SCNetworkSvgaWidget.isSvga(value)), ...candidates, ]); } String _firstNonBlank(Iterable values) { for (final value in values) { final text = value?.trim(); if (text != null && text.isNotEmpty) { return text; } } return ""; } void _showMsgItemMenu(BuildContext ct, String content) { SmartDialog.showAttach( tag: "showMsgItemMenu", targetContext: ct, alignment: Alignment.topCenter, maskColor: Colors.transparent, animationType: SmartAnimationType.fade, scalePointBuilder: (selfSize) => Offset(selfSize.width, 10), builder: (_) { return Container( margin: EdgeInsets.only(bottom: 10.w), decoration: BoxDecoration( color: Color(0xffdcdddf), borderRadius: BorderRadius.circular(10), ), padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), child: Row( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { SmartDialog.dismiss(tag: "showMsgItemMenu"); _delete(); }, child: Container( margin: EdgeInsets.symmetric(horizontal: 8.w), child: Column( children: [ Image.asset( _strategy.getSCMessageChatPageMessageMenuDeleteIcon(), width: 20.w, ), SizedBox(height: 3.w), text( SCAppLocalizations.of(context)!.delete, fontSize: 12.sp, textColor: Color(0xff777777), ), ], ), ), ), content.isNotEmpty ? GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { SmartDialog.dismiss(tag: "showMsgItemMenu"); Clipboard.setData(ClipboardData(text: content)); SCTts.show( SCAppLocalizations.of(context)!.copiedToClipboard, ); }, child: Container( margin: EdgeInsets.symmetric(horizontal: 8.w), child: Column( children: [ Image.asset( _strategy.getSCMessageChatPageMessageMenuCopyIcon(), width: 20.w, ), SizedBox(height: 3.w), text( SCAppLocalizations.of(context)!.copy, fontSize: 12.sp, textColor: Color(0xff777777), ), ], ), ), ) : Container(), (message.isSelf ?? false) ? GestureDetector( onTap: () { SmartDialog.dismiss(tag: "showMsgItemMenu"); _revokeMessage(); }, behavior: HitTestBehavior.opaque, child: Container( margin: EdgeInsets.symmetric(horizontal: 8.w), child: Column( children: [ Image.asset( _strategy .getSCMessageChatPageMessageMenuRecallIcon(), width: 20.w, ), SizedBox(height: 3.w), text( SCAppLocalizations.of(context)!.recall, fontSize: 12.sp, textColor: Color(0xff777777), ), ], ), ), ) : Container(), ], ), ); }, ); } ///撤回消息 void _revokeMessage() async { SmartDialog.show( tag: "showConfirmDialog", alignment: Alignment.center, debounce: true, animationType: SmartAnimationType.fade, builder: (_) { return MsgDialog( title: SCAppLocalizations.of(context)!.tips, msg: SCAppLocalizations.of(context)!.recallThisMessage, btnText: SCAppLocalizations.of(context)!.confirm, onEnsure: () async { // 撤回指定msgID的消息 V2TimCallback revokeMessage = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .revokeMessage(msgID: message.msgID ?? ""); await TencentImSDKPlugin.v2TIMManager .getMessageManager() .deleteMessages(msgIDs: [message.msgID ?? ""]); if (revokeMessage.code == 0) { // 撤回成功 int index = 0; for (var value in currentConversationMessageList) { if (value.msgID == message.msgID) { break; } index = index + 1; } currentConversationMessageList.removeAt(index); updateCall(); } else { // 撤回失败,可通过 revokeMessage.desc 获取错误描述 SCTts.show("fail:${revokeMessage.desc}"); } }, ); }, ); } ///删除消息 void _delete() { SmartDialog.show( tag: "showDeleteDialog", alignment: Alignment.bottomCenter, debounce: true, animationType: SmartAnimationType.fade, builder: (_) { return SafeArea( child: Container( height: 168.w, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(8.0), topRight: Radius.circular(8.0), ), ), child: Column( children: [ SizedBox(height: 10.w), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ text( SCAppLocalizations.of(context)!.tips, fontSize: 16.sp, textColor: Colors.black, fontWeight: FontWeight.bold, ), ], ), SizedBox(height: 5.w), Divider(height: height(1), color: SocialChatTheme.dividerColor), GestureDetector( child: Container( padding: EdgeInsets.symmetric(vertical: 10.w), color: Colors.transparent, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ text( SCAppLocalizations.of(context)!.deleteFromMyDevice, fontSize: 14.sp, textColor: Colors.black, ), ], ), ), onTap: () async { V2TimCallback deleteMessageFromLocalStorage = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .deleteMessageFromLocalStorage( msgID: message.msgID ?? "", ); if (deleteMessageFromLocalStorage.code == 0) { //删除成功 int index = 0; for (var value in currentConversationMessageList) { if (value.msgID == message.msgID) { break; } index = index + 1; } currentConversationMessageList.removeAt(index); updateCall(); SmartDialog.dismiss(tag: "showDeleteDialog"); } }, ), Divider(height: height(1), color: SocialChatTheme.dividerColor), GestureDetector( onTap: () async { V2TimCallback deleteMessageFromLocalStorage = await TencentImSDKPlugin.v2TIMManager .getMessageManager() .deleteMessages(msgIDs: [message.msgID ?? ""]); if (deleteMessageFromLocalStorage.code == 0) { //删除成功 int index = 0; for (var value in currentConversationMessageList) { if (value.msgID == message.msgID) { break; } index = index + 1; } currentConversationMessageList.removeAt(index); updateCall(); SmartDialog.dismiss(tag: "showDeleteDialog"); } }, child: Container( padding: EdgeInsets.symmetric(vertical: 10.w), color: Colors.transparent, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ text( SCAppLocalizations.of(context)!.deleteOnAllDevices, fontSize: 14.sp, textColor: Colors.black, ), ], ), ), ), Divider(height: height(1), color: SocialChatTheme.dividerColor), GestureDetector( onTap: () async { SmartDialog.dismiss(tag: "showDeleteDialog"); }, child: Container( padding: EdgeInsets.symmetric(vertical: 10.w), color: Colors.transparent, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ text( SCAppLocalizations.of(context)!.cancel, fontSize: 14.sp, textColor: Colors.black, ), ], ), ), ), ], ), ), ); }, ); } Future updateRedPacketStatus(String messageID, String packetID) async { // 构建扩展信息,例如标记为已领取,并包含领取者ID Map extensionMap = {"packetID": packetID}; List extensions = extensionMap.entries .map( (e) => V2TimMessageExtension( extensionKey: e.key, extensionValue: e.value, ), ) .toList(); await TencentImSDKPlugin.v2TIMManager .getMessageManager() .setMessageExtensions( msgID: messageID, // 原始红包消息的ID extensions: extensions, ); } void _showOpenDialog(SCUserRedPacketSendRes resData) { SmartDialog.show( tag: "showOpenDialog", alignment: Alignment.center, debounce: true, animationType: SmartAnimationType.fade, builder: (_) { return Container( height: 400.w, alignment: Alignment.topCenter, decoration: BoxDecoration( image: DecorationImage( image: AssetImage( _strategy.getSCMessageChatPageRedEnvelopeOpenBackground(), ), fit: BoxFit.fill, ), ), child: Column( children: [ SizedBox(height: 100.w), SCDebounceWidget( child: Container( alignment: Alignment.center, width: 60.w, height: 60.w, color: Colors.transparent, child: text( SCAppLocalizations.of(context)!.open, textColor: Colors.white, fontSize: 16.sp, fontWeight: FontWeight.w900, ), ), onTap: () { SmartDialog.dismiss(tag: "showOpenDialog"); SCLoadingManager.show(); SCAccountRepository() .userRedPacketGrab(resData.packetId ?? "") .then((res) { var future = SCAccountRepository().userRedPacketDetail( resData.packetId ?? "", ); updateRedPacketStatus( message.msgID ?? "", resData.packetId ?? "", ); SCMessageUtils.redPacketFutureCache[resData.packetId ?? ""] = future; future .then((res2) { SCLoadingManager.hide(); updateCall(); _showOpenedDialog(res2); }) .catchError((e) { SCLoadingManager.hide(); }); }) .catchError((e) { SCLoadingManager.hide(); }); }, ), SizedBox(height: 35.w), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ text( SCAppLocalizations.of(context)!.openRedPackDialogTip, fontWeight: FontWeight.w600, textColor: Colors.white, fontSize: 14.sp, ), SizedBox(width: 3.w), Container( margin: EdgeInsets.only(bottom: 1.w), height: 18.w, constraints: BoxConstraints(maxWidth: 65.w), child: (resData.senderInfo?.nickName?.length ?? 0) > 6 ? Marquee( text: resData.senderInfo?.nickName ?? "", style: TextStyle( fontSize: 14.sp, color: Colors.white, fontWeight: FontWeight.w600, decoration: TextDecoration.none, ), scrollAxis: Axis.horizontal, crossAxisAlignment: CrossAxisAlignment.start, blankSpace: 20.0, velocity: 40.0, pauseAfterRound: Duration(seconds: 1), accelerationDuration: Duration(seconds: 1), accelerationCurve: Curves.easeOut, decelerationDuration: Duration(milliseconds: 550), decelerationCurve: Curves.easeOut, ) : Text( resData.senderInfo?.nickName ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 14.sp, color: Colors.white, fontWeight: FontWeight.w600, decoration: TextDecoration.none, ), ), ), SizedBox(width: 3.w), Container( child: text( SCAppLocalizations.of(context)!.sent, fontWeight: FontWeight.w600, textColor: Colors.white, fontSize: 14.sp, ), height: 18.w, ), ], ), SizedBox(height: 15.w), Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ text( SCAppLocalizations.of(context)!.wishingYouHappinessEveryDay, fontWeight: FontWeight.w600, textColor: Colors.white70, fontSize: 13.sp, ), ], ), ], ), ); }, ); } void _showOpenedDialog(SCUserRedPacketSendRes resData) { SmartDialog.show( tag: "showOpenedDialog", alignment: Alignment.center, debounce: true, animationType: SmartAnimationType.fade, builder: (_) { return Container( margin: EdgeInsets.symmetric(horizontal: 20.w), decoration: BoxDecoration( image: DecorationImage( image: AssetImage( _strategy.getSCMessageChatPageRedEnvelopeOpenedBackground(), ), fit: BoxFit.fill, ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 70.w), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ GestureDetector( child: Icon(Icons.close, size: 18.w, color: Colors.white), onTap: () { SmartDialog.dismiss(tag: "showOpenedDialog"); }, ), SizedBox(width: 10.w), ], ), SizedBox(height: 25.w), Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(width: 20.w), netImage( url: resData.senderInfo?.avatar ?? "", shape: BoxShape.circle, width: 45.w, height: 45.w, ), SizedBox(width: 10.w), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( height: 18.w, constraints: BoxConstraints(maxWidth: 85.w), child: (resData.senderInfo?.nickName?.length ?? 0) > 8 ? Marquee( text: resData.senderInfo?.nickName ?? "", style: TextStyle( fontSize: 14.sp, color: Colors.white, fontWeight: FontWeight.w600, decoration: TextDecoration.none, ), scrollAxis: Axis.horizontal, crossAxisAlignment: CrossAxisAlignment.start, blankSpace: 20.0, velocity: 40.0, pauseAfterRound: Duration(seconds: 1), accelerationDuration: Duration( seconds: 1, ), accelerationCurve: Curves.easeOut, decelerationDuration: Duration( milliseconds: 500, ), decelerationCurve: Curves.easeOut, ) : Text( resData.senderInfo?.nickName ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 14.sp, color: Colors.white, fontWeight: FontWeight.w600, decoration: TextDecoration.none, ), ), ), SizedBox(width: 5.w), Container( child: text( SCAppLocalizations.of(context)!.sentARedEnvelope, fontWeight: FontWeight.w600, textColor: Colors.white, fontSize: 14.sp, ), height: 18.w, ), ], ), SizedBox(height: 5.w), Row( children: [ text( SCAppLocalizations.of( context, )!.wishingYouHappinessEveryDay, fontWeight: FontWeight.w600, textColor: Colors.white70, fontSize: 13.sp, ), ], ), ], ), ], ), SizedBox(height: 10.w), AccountStorage().getCurrentUser()?.userProfile?.id == "${resData.receiverInfo?.userId}" ? Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( _strategy.getSCMessageChatPageGoldCoinIcon(), height: 35.w, ), text( "${resData.totalAmount ?? 0}", fontWeight: FontWeight.w900, textColor: Color(0xffFFF836), fontSize: 22.sp, ), ], ) : Container(), Divider( color: Color(0xffF3F3F3), endIndent: 15.w, indent: 15.w, thickness: 0.4, ), SizedBox(height: 5.w), Row( children: [ SizedBox(width: 15.w), text( SCAppLocalizations.of( context, )!.redEnvelopeAmount("${resData.totalAmount}"), fontWeight: FontWeight.w600, textColor: Colors.white70, fontSize: 13.sp, ), Spacer(), resData.status == 2 ? Row( children: [ Image.asset( _strategy.getSCMessageChatPageGoldCoinIcon(), height: 15.w, ), text( "${resData.totalAmount ?? 0}", fontWeight: FontWeight.w400, textColor: Color(0xffFFF836), fontSize: 14.sp, ), ], ) : Container(), SizedBox(width: 15.w), ], ), SizedBox(height: 8.w), resData.status == 2 ? Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox(width: 20.w), netImage( url: resData.receiverInfo?.avatar ?? "", shape: BoxShape.circle, width: 45.w, height: 45.w, ), SizedBox(width: 10.w), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( height: 18.w, constraints: BoxConstraints(maxWidth: 120.w), child: (resData.receiverInfo?.nickName?.length ?? 0) > 12 ? Marquee( text: resData.receiverInfo?.nickName ?? "", style: TextStyle( fontSize: 14.sp, color: Colors.white, fontWeight: FontWeight.w600, decoration: TextDecoration.none, ), scrollAxis: Axis.horizontal, crossAxisAlignment: CrossAxisAlignment.start, blankSpace: 20.0, velocity: 40.0, pauseAfterRound: Duration(seconds: 1), accelerationDuration: Duration( seconds: 1, ), accelerationCurve: Curves.easeOut, decelerationDuration: Duration( milliseconds: 500, ), decelerationCurve: Curves.easeOut, ) : Text( resData.receiverInfo?.nickName ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 14.sp, color: Colors.white, fontWeight: FontWeight.w600, decoration: TextDecoration.none, ), ), ), SizedBox(width: 5.w), ], ), SizedBox(height: 5.w), Row( children: [ text( SCMDateUtils.formatDateTime3( DateTime.fromMillisecondsSinceEpoch( resData.grabbedAt ?? 0, ), ), fontWeight: FontWeight.w600, textColor: Colors.white70, fontSize: 13.sp, ), ], ), ], ), ], ) : (resData.status == 1 ? Row( mainAxisAlignment: MainAxisAlignment.center, children: [ text( SCAppLocalizations.of( context, )!.redEnvelopeNotYetClaimed, fontWeight: FontWeight.w600, textColor: Colors.white, fontSize: 15.sp, ), ], ) : Container()), SizedBox(height: 18.w), ], ), ); }, ); } _buildPackTips(SCUserRedPacketSendRes? resData) { //状态:1-待领取 2-已领取 3-已过期 if (resData?.status == 2) { if ("${resData?.receiverInfo?.userId}" == AccountStorage().getCurrentUser()?.userProfile?.id) { return Container( width: ScreenUtil().screenWidth * 0.6, child: Text.rich( TextSpan( children: [ TextSpan( text: SCAppLocalizations.of( context, )!.youAccepted(resData?.senderInfo?.nickName ?? ""), style: TextStyle( fontSize: 12.sp, color: Colors.black26, fontWeight: FontWeight.bold, ), ), TextSpan( text: SCAppLocalizations.of(context)!.redEnvelope, style: TextStyle( fontSize: 12.sp, color: Color(0xffFF7759), fontWeight: FontWeight.w600, ), ), TextSpan( text: ".", style: TextStyle( fontSize: 12.sp, color: Color(0xffFF7759), fontWeight: FontWeight.bold, ), ), ], ), ), ); } else if ("${resData?.senderInfo?.userId}" == AccountStorage().getCurrentUser()?.userProfile?.id) { return Container( width: ScreenUtil().screenWidth * 0.6, child: Text.rich( TextSpan( children: [ TextSpan( text: SCAppLocalizations.of( context, )!.acceptedYour(resData?.receiverInfo?.nickName ?? ""), style: TextStyle( fontSize: 12.sp, color: Colors.black26, fontWeight: FontWeight.bold, ), ), TextSpan( text: SCAppLocalizations.of(context)!.redEnvelope, style: TextStyle( fontSize: 12.sp, color: Color(0xffFF7759), fontWeight: FontWeight.w600, ), ), TextSpan( text: ".", style: TextStyle( fontSize: 12.sp, color: Color(0xffFF7759), fontWeight: FontWeight.bold, ), ), ], ), ), ); } } return Container(); } } // class MySpecialTextSpanBuilder extends SpecialTextSpanBuilder { // /// whether show background for @somebody // MySpecialTextSpanBuilder(); // // @override // TextSpan build(String data, {TextStyle? textStyle, onTap}) { // var textSpan = super.build(data, textStyle: textStyle, onTap: onTap); // return textSpan; // } // // @override // SpecialText? createSpecialText( // String flag, { // TextStyle? textStyle, // SpecialTextGestureTapCallback? onTap, // required int index, // }) { // if (flag == null || flag == "") return null; // // ///index is end index of start flag, so text start index should be index-(flag.length-1) // if (textStyle != null) { // if (isStart(flag, EmojiText.flag)) { // return EmojiText(textStyle, start: index - (EmojiText.flag.length - 1)); // } // } // return null; // } // } // class EmojiText extends SpecialText { // EmojiText(TextStyle textStyle, {required this.start}) // : super(EmojiText.flag, ']', textStyle); // static const String flag = '['; // final int start; // // @override // InlineSpan finishText() { // String key = toString(); // key = key.replaceAll("[", ""); // key = key.replaceAll("]", ""); // int index; // try { // index = int.parse(key); // } catch (e) { // return TextSpan(text: toString(), style: textStyle); // } // // ///https://github.com/flutter/flutter/issues/42086 // /// widget span is not working on web // if (index <= 113) { // //fontsize id define image height // //size = 30.0/26.0 * fontSize // double size = 16.w; // // ///fontSize 26 and text height =30.0 // //final double fontSize = 26.0; // return ImageSpan( // AssetImage("sc_images/msg/emoji/$key.png"), // actualText: key, // imageWidth: textStyle!.fontSize!, // imageHeight: textStyle!.fontSize!, // start: start, // fit: BoxFit.fill, // margin: const EdgeInsets.only(left: 2.0, top: 2.0, right: 2.0), // ); // } // // return TextSpan(text: toString(), style: textStyle); // } // }