yumi-flutter/lib/modules/chat/message_chat_page.dart
2026-06-03 18:46:16 +08:00

3850 lines
133 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/tools/sc_chat_bubble_message_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/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/user_manager.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);
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<SCMessageChatPage> {
BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy;
final TextEditingController _textController = TextEditingController();
final ScrollController _scrollController = ScrollController();
final RefreshController _refreshController = RefreshController();
// FlutterSoundPlayer? flutterSound;
RtmProvider? rtmProvider;
V2TimConversation? currentConversation;
List<V2TimMessage> currentConversationMessageList = [];
final FocusNode _focusNode = FocusNode();
///是否显示工具栏
bool showTools = false;
bool showEmoji = false;
bool showDoNotClickUnfamiliarTips = true;
///互相关注才能互相发送消息
bool canSendMsg = false;
SocialChatUserProfile? friend;
final Map<String, _CpInviteMessageState> _localCpInviteStates = {};
///是否显示发送按钮
bool showSend = false;
List<String> 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<RtmProvider>(context, listen: false);
currentConversation = widget.conversation;
rtmProvider?.onMessageRecvC2CReadListener = _onMessageRecvC2CRead;
rtmProvider?.onRevokeMessageListener = _onRevokeMessage;
rtmProvider?.onNewMessageCurrentConversationListener = _onNewMessage;
loadMsg();
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<RtmProvider>(
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: <Widget>[
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: <Widget>[
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();
});
}
///消息列表
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],
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<V2TimMessage> _messagesInDisplayOrder(List<V2TimMessage> 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<V2TimMessage> 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<List<V2TimMessage>> _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<void> _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<void> 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<SocialChatUserProfileManager>(
builder:
(_, provider, __) => Container(
color: Colors.transparent,
height: 56.w,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
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<RtmProvider>(
context,
listen: false,
).sendC2CTextMsg(msg, currentConversation!);
}
///发消息
sendCustomMsg(String msg, String extension) async {
if (!canSendMsg) {
SCTts.show(SCAppLocalizations.of(context)!.canSendMsgTips);
return;
}
Provider.of<RtmProvider>(
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: <Widget>[
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<File>? 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<String> 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 V2TimConversation? currentConversation;
BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy;
// final FTIMUserProfile userProfile;
final SocialChatUserProfile? friend;
bool isSystem = false;
List<V2TimMessage> currentConversationMessageList = [];
final Map<String, _CpInviteMessageState> localCpInviteStates;
final void Function(String applyId, _CpInviteMessageState state)?
onCpInviteStateChanged;
Function updateCall;
///上一条
late BuildContext context;
_MessageItem({
required this.message,
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 isCpInviteMessage =
customData != null && _isCpInvitePayload(customData);
final isExplicitCpResultMessage =
isCpInviteMessage &&
_cpInviteStateFromData(customData) != _CpInviteMessageState.pending;
final cpRelationFormedCard =
isExplicitCpResultMessage ? _cpRelationFormedMessageCard() : null;
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: <Widget>[
Visibility(
visible: showTime,
child: Container(
margin: EdgeInsets.only(bottom: 12.w),
child: Row(
children: <Widget>[
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 ||
(isCpInviteMessage && !isExplicitCpResultMessage))
Padding(
padding: EdgeInsets.only(
top: cpStatusLineText == null ? 0 : 10.w,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
textDirection:
message.isSelf! ? TextDirection.rtl : TextDirection.ltr,
children: <Widget>[
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: <Widget>[
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<String, dynamic> 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<String, dynamic> data,
SCSystemInvitMessageRes? cpInvite,
RoomCpInviteActionState actionState,
) {
unawaited(_openCpInviteDialogFromMessageAsync(data, cpInvite, actionState));
}
Future<void> _openCpInviteDialogFromMessageAsync(
Map<String, dynamic> 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 (_cpInviteStateFromData(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 null;
}
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? _cpInviteDialogStatusText(
Map<String, dynamic> data,
RoomCpInviteActionState actionState,
) {
switch (actionState) {
case RoomCpInviteActionState.rejected:
return _cpRejectedStatusText(data);
case RoomCpInviteActionState.expired:
case RoomCpInviteActionState.accepted:
case RoomCpInviteActionState.pending:
return null;
}
}
String _cpRejectedStatusText(Map<String, dynamic> data) {
final localizations = SCAppLocalizations.of(context)!;
final relationType = _cpRelationTypeFromMessageData(data);
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.";
}
void _showCpInviteDialogFromMessage(
Map<String, dynamic> 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),
statusText: _cpInviteDialogStatusText(data, effectiveActionState),
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<String, dynamic> data,
) {
unawaited(_processCpInviteFromMessageAsync(applyId, invite, agree, data));
}
Future<void> _processCpInviteFromMessageAsync(
String applyId,
SCSystemInvitMessageRes invite,
bool agree,
Map<String, dynamic> 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<SocialChatUserProfileManager>(
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<String, dynamic> 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<String, dynamic> 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<String, dynamic>? _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<String, dynamic> 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<String, dynamic> 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;
var hasAcceptedSiblingInvite = false;
for (final item in currentConversationMessageList) {
if ((item.timestamp ?? 0) < currentTimestamp) {
continue;
}
final data = _customDataFromMessage(item);
if (data == null || !_isCpInvitePayload(data)) {
continue;
}
final state = _cpInviteStateFromData(data);
final itemApplyId = _cpApplyIdFromData(data);
if (itemApplyId == applyId) {
if (state != _CpInviteMessageState.pending) {
return state;
}
continue;
}
if (state == _CpInviteMessageState.accepted &&
_isSameCpInvitePair(currentData, data)) {
hasAcceptedSiblingInvite = true;
}
}
if (hasAcceptedSiblingInvite) {
return _CpInviteMessageState.rejected;
}
return _CpInviteMessageState.pending;
}
bool _isSameCpInvitePair(
Map<String, dynamic> left,
Map<String, dynamic> right,
) {
final leftInviterIds = _cpInviteInviterIdentityValues(left);
final leftReceiverIds = _cpInviteReceiverIdentityValues(left);
final rightInviterIds = _cpInviteInviterIdentityValues(right);
final rightReceiverIds = _cpInviteReceiverIdentityValues(right);
if (leftInviterIds.isEmpty ||
leftReceiverIds.isEmpty ||
rightInviterIds.isEmpty ||
rightReceiverIds.isEmpty) {
return false;
}
final sameDirection =
_hasAnyCommonCpIdentity(leftInviterIds, rightInviterIds) &&
_hasAnyCommonCpIdentity(leftReceiverIds, rightReceiverIds);
final reversedDirection =
_hasAnyCommonCpIdentity(leftInviterIds, rightReceiverIds) &&
_hasAnyCommonCpIdentity(leftReceiverIds, rightInviterIds);
return sameDirection || reversedDirection;
}
bool _hasAnyCommonCpIdentity(Set<String> left, Set<String> right) {
if (left.isEmpty || right.isEmpty) {
return false;
}
return left.any(right.contains);
}
_CpInviteMessageState? _localCpInviteStateForData(Map<String, dynamic> data) {
return _localCpInviteStateForApplyId(_cpApplyIdFromData(data));
}
_CpInviteMessageState? _localCpInviteStateForApplyId(String applyId) {
final normalizedApplyId = applyId.trim();
if (normalizedApplyId.isEmpty) {
return null;
}
return localCpInviteStates[normalizedApplyId];
}
bool _isDuplicateCpInviteMessageForConversation(Map<String, dynamic> 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<String, dynamic> data) {
if (!_isCpInvitePayload(data)) {
return "";
}
final state = _cpInviteStateFromData(data);
if (state == _CpInviteMessageState.pending) {
return "";
}
final applyId = _cpApplyIdFromData(data);
if (applyId.isEmpty) {
return "";
}
return "$applyId|${state.name}";
}
_CpInviteMessageState _cpInviteStateFromData(Map<String, dynamic> data) {
final content = _cpContentMap(data);
final values = <String>[
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<dynamic> 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<String, dynamic> data) {
final content = _cpContentMap(data);
final noticeType =
<String>[
data["noticeType"]?.toString() ?? "",
data["type"]?.toString() ?? "",
content["noticeType"]?.toString() ?? "",
content["type"]?.toString() ?? "",
].join("|").toUpperCase();
return noticeType.contains(SCSysytemMessageType.CP_BUILD.name);
}
bool _isCurrentUserCpInviter(Map<String, dynamic> 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<String> _cpCurrentProfileIds(SocialChatUserProfile currentProfile) {
return [currentProfile.id, currentProfile.account]
.map((value) => value?.trim() ?? "")
.where((value) => value.isNotEmpty)
.toSet();
}
Set<String> _cpInviteInviterIdentityValues(Map<String, dynamic> data) {
final content = _cpContentMap(data);
final values = <String?>[
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<String> _cpInviteReceiverIdentityValues(Map<String, dynamic> 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<String, dynamic> 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<dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> _cpContentMap(Map<String, dynamic> 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 <String, dynamic>{};
}
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<String?> 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<String> 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<V2TimValueCallback<V2TimMessageOnlineUrl>>(
// 假设 getMessageOnlineUrlForMessage 是对 TencentImSDKPlugin.v2TIMManager.getMessageManager().getMessageOnlineUrl 的封装,返回 Future<String>
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 = scDecodeChatBubbleFromCloudCustomData(
message.cloudCustomData,
);
final chatBubbleUrl = _resolveChatBubbleImageUrl(chatBubble);
if (chatBubbleUrl.isNotEmpty) {
return _buildVipTextBubble(content: content, imageUrl: chatBubbleUrl);
}
return _buildDefaultTextBubble(content);
}
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<double>(
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 = <String?>[
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<String?> 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<void> updateRedPacketStatus(String messageID, String packetID) async {
// 构建扩展信息例如标记为已领取并包含领取者ID
Map<String, String> extensionMap = {"packetID": packetID};
List<V2TimMessageExtension> 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);
// }
// }