3901 lines
134 KiB
Dart
3901 lines
134 KiB
Dart
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 = <String?>[
|
||
resource?.sourceResourceUrl,
|
||
resource?.previewUrl,
|
||
resource?.coverUrl,
|
||
resource?.cover,
|
||
];
|
||
return _firstNonBlankChatBubbleUrl([
|
||
...candidates.where((value) => SCNetworkSvgaWidget.isSvga(value)),
|
||
...candidates,
|
||
]);
|
||
}
|
||
|
||
String _firstNonBlankChatBubbleUrl(Iterable<String?> 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<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;
|
||
SCVipResourceRes? _selfVipChatBubble;
|
||
bool _isSelfVipChatBubbleLoading = false;
|
||
|
||
///是否显示发送按钮
|
||
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();
|
||
_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<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();
|
||
});
|
||
}
|
||
|
||
Future<void> _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<SCVipResourceRes?> 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,
|
||
updateCall: () {
|
||
setState(() {});
|
||
},
|
||
),
|
||
childCount: currentConversationMessageList.length,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
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 {
|
||
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<V2TimMessage> currentConversationMessageList = [];
|
||
Function updateCall;
|
||
|
||
///上一条
|
||
late BuildContext context;
|
||
|
||
_MessageItem({
|
||
required this.message,
|
||
this.selfVipChatBubble,
|
||
this.currentConversation,
|
||
this.preMessage,
|
||
// this.userProfile,
|
||
this.isSystem = false,
|
||
required this.currentConversationMessageList,
|
||
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 &&
|
||
_isDuplicateCpResultMessageForConversation(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: <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 (cpRelationFormedCard != null)
|
||
cpRelationFormedCard
|
||
else if (cpStatusLineText != null)
|
||
_buildCpStatusLine(cpStatusLineText)
|
||
else
|
||
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 {
|
||
final latestState = await _latestCpInviteStateForMessage(data);
|
||
if (!context.mounted) {
|
||
return;
|
||
}
|
||
final resolvedActionState =
|
||
latestState == _CpInviteMessageState.pending
|
||
? actionState
|
||
: _cpInviteActionState(latestState);
|
||
_showCpInviteDialogFromMessage(data, cpInvite, resolvedActionState);
|
||
}
|
||
|
||
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 = _cpInviteStateFromData(data);
|
||
if (state == _CpInviteMessageState.pending ||
|
||
state == _CpInviteMessageState.accepted) {
|
||
return null;
|
||
}
|
||
final localizations = SCAppLocalizations.of(context)!;
|
||
final relationType = _cpRelationTypeFromMessageData(data);
|
||
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.";
|
||
}
|
||
|
||
void _showCpInviteDialogFromMessage(
|
||
Map<String, dynamic> data,
|
||
SCSystemInvitMessageRes? cpInvite,
|
||
RoomCpInviteActionState 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: actionState,
|
||
countdownSeconds: _cpInviteSecondsLeft(data),
|
||
descriptionText: _cpInviteDescriptionText(relationType),
|
||
onAccept:
|
||
!isInviter &&
|
||
cpInvite != null &&
|
||
actionState == RoomCpInviteActionState.pending &&
|
||
applyId.isNotEmpty
|
||
? () => _processCpInviteFromMessage(applyId, cpInvite, true, data)
|
||
: null,
|
||
onReject:
|
||
!isInviter &&
|
||
cpInvite != null &&
|
||
actionState == 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);
|
||
_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;
|
||
}
|
||
|
||
_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 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 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;
|
||
}
|
||
|
||
bool _isDuplicateCpResultMessageForConversation(Map<String, dynamic> data) {
|
||
final currentKey = _cpResultMessageDedupKey(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 (_cpResultMessageDedupKey(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 _cpResultMessageDedupKey(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"],
|
||
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 =
|
||
(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<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);
|
||
// }
|
||
// }
|