diff --git a/lib/modules/chat/message_chat_page.dart b/lib/modules/chat/message_chat_page.dart index 6c3469f..49e5eca 100644 --- a/lib/modules/chat/message_chat_page.dart +++ b/lib/modules/chat/message_chat_page.dart @@ -368,7 +368,7 @@ class _SCMessageChatPageState extends State { } SCLoadingManager.show(); Future.wait([ - SCAccountRepository().loadUserInfo(userId), + SCAccountRepository().loadUserInfo(userId, silentErrorToast: true), SCAccountRepository().friendRelationCheck(userId), ]) .then((result) { @@ -593,6 +593,7 @@ class _SCMessageChatPageState extends State { (c, i) => _MessageItem( message: currentConversationMessageList[i], selfVipChatBubble: _selfVipChatBubble, + currentConversation: currentConversation, preMessage: i > 0 ? currentConversationMessageList[i - 1] : null, isSystem: @@ -1164,6 +1165,7 @@ class _MessageItem extends StatelessWidget { final V2TimMessage message; final V2TimMessage? preMessage; final SCVipResourceRes? selfVipChatBubble; + final V2TimConversation? currentConversation; BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; @@ -1179,6 +1181,7 @@ class _MessageItem extends StatelessWidget { _MessageItem({ required this.message, this.selfVipChatBubble, + this.currentConversation, this.preMessage, // this.userProfile, this.isSystem = false, @@ -1529,12 +1532,15 @@ class _MessageItem extends StatelessWidget { 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(), @@ -1545,8 +1551,10 @@ class _MessageItem extends StatelessWidget { content["acceptUserId"]?.toString(), content["receiverActualAccount"]?.toString(), content["receiverUserId"]?.toString(), + friend?.id, + currentConversation?.userID, ], fallback: ""), - fallback: currentName, + fallback: currentConversation?.userID ?? "User", ); final receiverAvatar = _firstNonBlankString([ data["acceptUserAvatar"]?.toString(), @@ -1555,7 +1563,9 @@ class _MessageItem extends StatelessWidget { content["acceptUserAvatar"]?.toString(), content["receiverAvatar"]?.toString(), content["receiverUserAvatar"]?.toString(), - ], fallback: currentProfile?.userAvatar ?? ""); + friend?.userAvatar, + currentConversation?.faceUrl, + ], fallback: ""); final isInviter = _isCurrentUserCpInviter(data); final relationType = scNormalizeCpRelationType( _firstNonBlankString([ @@ -1579,12 +1589,18 @@ class _MessageItem extends StatelessWidget { rightName: isInviter ? receiverName : currentName, leftAvatar: isInviter - ? currentProfile?.userAvatar ?? "" + ? _firstNonBlankString([ + currentProfile?.userAvatar, + inviterAvatar, + ]) : inviterAvatar, rightAvatar: isInviter ? receiverAvatar - : currentProfile?.userAvatar ?? "", + : _firstNonBlankString([ + currentProfile?.userAvatar, + receiverAvatar, + ]), relationType: relationType, ), ), @@ -1631,26 +1647,73 @@ class _MessageItem extends StatelessWidget { ); 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: "User", + fallback: currentConversation?.userID ?? "User", ) : currentName; final relationType = scNormalizeCpRelationType(cpInvite?.relationType); @@ -1685,29 +1748,25 @@ class _MessageItem extends StatelessWidget { name: receiverName, avatarUrl: isInviter - ? _firstNonBlankString([ - data["acceptUserAvatar"]?.toString(), - content["acceptUserAvatar"]?.toString(), - ], fallback: "") - : currentProfile?.userAvatar ?? "", + ? receiverAvatar + : _firstNonBlankString([ + currentProfile?.userAvatar, + receiverAvatar, + ]), headdress: isInviter - ? _firstNonBlankString([ - data["acceptHeaddress"]?.toString(), - data["acceptAvatarFrame"]?.toString(), - content["acceptHeaddress"]?.toString(), - content["acceptAvatarFrame"]?.toString(), - ], fallback: "") - : currentProfile?.getHeaddress()?.sourceUrl ?? "", + ? receiverHeaddress + : _firstNonBlankString([ + currentProfile?.getHeaddress()?.sourceUrl, + receiverHeaddress, + ]), headdressCover: isInviter - ? _firstNonBlankString([ - data["acceptHeaddressCover"]?.toString(), - data["acceptAvatarFrameCover"]?.toString(), - content["acceptHeaddressCover"]?.toString(), - content["acceptAvatarFrameCover"]?.toString(), - ], fallback: "") - : currentProfile?.getHeaddress()?.cover ?? "", + ? receiverHeaddressCover + : _firstNonBlankString([ + currentProfile?.getHeaddress()?.cover, + receiverHeaddressCover, + ]), ), style: isInviter @@ -1934,23 +1993,91 @@ class _MessageItem extends StatelessWidget { if (currentProfile == null) { return false; } + final currentIds = _cpCurrentProfileIds(currentProfile); + if (currentIds.isEmpty) { + return false; + } + final receiverIds = _cpInviteReceiverIdentityValues(data); + if (currentIds.any(receiverIds.contains)) { + return false; + } + final inviterIds = _cpInviteInviterIdentityValues(data); + return currentIds.any(inviterIds.contains); + } + + Set _cpCurrentProfileIds(SocialChatUserProfile currentProfile) { + return [currentProfile.id, currentProfile.account] + .map((value) => value?.trim() ?? "") + .where((value) => value.isNotEmpty) + .toSet(); + } + + Set _cpInviteInviterIdentityValues(Map data) { final content = _cpContentMap(data); - final inviterIds = - [ - data["applyUserId"]?.toString() ?? "", - data["applyUserAccount"]?.toString() ?? "", - data["account"]?.toString() ?? "", - data["actualAccount"]?.toString() ?? "", - content["applyUserId"]?.toString() ?? "", - content["applyUserAccount"]?.toString() ?? "", - content["account"]?.toString() ?? "", - content["actualAccount"]?.toString() ?? "", - ] - .map((value) => value.trim()) - .where((value) => value.isNotEmpty) - .toSet(); - return inviterIds.contains(currentProfile.id?.trim() ?? "") || - inviterIds.contains(currentProfile.account?.trim() ?? ""); + final values = [ + data["account"]?.toString(), + data["actualAccount"]?.toString(), + data["applyUserId"]?.toString(), + data["applyUserAccount"]?.toString(), + data["applyActualAccount"]?.toString(), + data["applyUserActualAccount"]?.toString(), + data["inviterActualAccount"]?.toString(), + data["inviterUserId"]?.toString(), + data["inviterId"]?.toString(), + data["senderUserId"]?.toString(), + data["senderId"]?.toString(), + data["fromUserId"]?.toString(), + content["applyUserId"]?.toString(), + content["applyUserAccount"]?.toString(), + content["applyActualAccount"]?.toString(), + content["applyUserActualAccount"]?.toString(), + content["inviterActualAccount"]?.toString(), + content["inviterUserId"]?.toString(), + content["inviterId"]?.toString(), + content["senderUserId"]?.toString(), + content["senderId"]?.toString(), + content["fromUserId"]?.toString(), + ]; + final explicitValues = + values.map((value) => value?.trim() ?? "").where((value) { + return value.isNotEmpty; + }).toSet(); + if (explicitValues.isNotEmpty) { + return explicitValues; + } + return [ + content["account"]?.toString(), + content["actualAccount"]?.toString(), + content["userId"]?.toString(), + ].map((value) => value?.trim() ?? "").where((value) { + return value.isNotEmpty; + }).toSet(); + } + + Set _cpInviteReceiverIdentityValues(Map data) { + final content = _cpContentMap(data); + return [ + data["acceptUserId"]?.toString(), + data["acceptAccount"]?.toString(), + data["acceptActualAccount"]?.toString(), + data["receiverId"]?.toString(), + data["receiverUserId"]?.toString(), + data["receiverAccount"]?.toString(), + data["receiverActualAccount"]?.toString(), + data["targetUserId"]?.toString(), + data["toUserId"]?.toString(), + content["acceptUserId"]?.toString(), + content["acceptAccount"]?.toString(), + content["acceptActualAccount"]?.toString(), + content["receiverId"]?.toString(), + content["receiverUserId"]?.toString(), + content["receiverAccount"]?.toString(), + content["receiverActualAccount"]?.toString(), + content["targetUserId"]?.toString(), + content["toUserId"]?.toString(), + ].map((value) => value?.trim() ?? "").where((value) { + return value.isNotEmpty; + }).toSet(); } int _cpInviteSecondsLeft(Map data) { diff --git a/lib/modules/gift/cp_rights/cp_rights_guide_page.dart b/lib/modules/gift/cp_rights/cp_rights_guide_page.dart index 6c15fba..a70e3cf 100644 --- a/lib/modules/gift/cp_rights/cp_rights_guide_page.dart +++ b/lib/modules/gift/cp_rights/cp_rights_guide_page.dart @@ -10,7 +10,6 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; -import 'package:yumi/ui_kit/components/sc_compontent.dart'; enum _CpGuideTab { note, rights } @@ -593,7 +592,6 @@ class _CpRightsGuidePageState extends State { ? "Lv.$levelNumber" : "Lv.${rows.length + 1}"), intimacy: _formatRightsIntimacyValue(requiredValue), - rights: level.rights.map(_CpRightItem.fromRes).toList(), ), ); } @@ -630,23 +628,16 @@ class _CpRightsGuidePageState extends State { } Widget _buildRightsTable(List<_CpRightsLevel> rows) { + const tableWidth = 351.0; + const columnWidth = tableWidth / 2; const headerHeight = 39.0; - const rightCellHeight = 77.0; - const dividerHeight = 1.0; - final tableHeight = - headerHeight + - rows.fold( - 0, - (height, row) => - height + - row.rightSlots * rightCellHeight + - (row.rightSlots - 1) * dividerHeight, - ); + const rowHeight = 77.0; + final tableHeight = headerHeight + rows.length * rowHeight; return ClipRRect( borderRadius: BorderRadius.circular(12.w), child: Container( - width: 351.w, + width: tableWidth.w, height: tableHeight.w, color: Colors.white.withValues(alpha: 0.3), child: Stack( @@ -654,15 +645,15 @@ class _CpRightsGuidePageState extends State { Positioned( top: 0, bottom: 0, - left: 117.w, - width: 117.w, + left: columnWidth.w, + width: columnWidth.w, child: ColoredBox(color: Colors.white.withValues(alpha: 0.24)), ), Positioned( left: 0, right: 0, top: headerHeight.w, - child: _tableLine(width: 351), + child: _tableLine(width: tableWidth), ), Positioned.fill( child: Column( @@ -671,9 +662,8 @@ class _CpRightsGuidePageState extends State { height: headerHeight.w, child: Row( children: [ - _tableHeader("Level"), - _tableHeader("Intimacy Level"), - _tableHeader("Rights"), + _tableHeader("Level", columnWidth), + _tableHeader("Intimacy Level", columnWidth), ], ), ), @@ -687,9 +677,9 @@ class _CpRightsGuidePageState extends State { ); } - Widget _tableHeader(String value) { + Widget _tableHeader(String value, double width) { return SizedBox( - width: 117.w, + width: width.w, child: Center( child: Text( value, @@ -701,41 +691,18 @@ class _CpRightsGuidePageState extends State { } Widget _buildRightsLevelRow(_CpRightsLevel row) { - const rightCellHeight = 77.0; - const dividerHeight = 1.0; - final groupHeight = - row.rightSlots * rightCellHeight + (row.rightSlots - 1) * dividerHeight; + const tableWidth = 351.0; + const columnWidth = tableWidth / 2; + const rowHeight = 77.0; return SizedBox( - height: groupHeight.w, + height: rowHeight.w, child: Stack( children: [ Positioned.fill( child: Row( children: [ - _spanCell(row.level, groupHeight), - _spanCell(row.intimacy, groupHeight), - SizedBox(width: 117.w, height: groupHeight.w), - ], - ), - ), - Positioned( - right: 0, - top: 0, - width: 117.w, - height: groupHeight.w, - child: Column( - children: [ - for (var i = 0; i < row.rightSlots; i++) ...[ - SizedBox( - height: rightCellHeight.w, - child: Center( - child: _buildRightCell( - i < row.rights.length ? row.rights[i] : null, - ), - ), - ), - if (i != row.rightSlots - 1) _tableLine(width: 117), - ], + _spanCell(row.level, width: columnWidth, height: rowHeight), + _spanCell(row.intimacy, width: columnWidth, height: rowHeight), ], ), ), @@ -743,16 +710,20 @@ class _CpRightsGuidePageState extends State { left: 0, right: 0, bottom: 0, - child: _tableLine(width: 351), + child: _tableLine(width: tableWidth), ), ], ), ); } - Widget _spanCell(String value, double height) { + Widget _spanCell( + String value, { + required double width, + required double height, + }) { return SizedBox( - width: 117.w, + width: width.w, height: height.w, child: Center( child: Text( @@ -768,61 +739,6 @@ class _CpRightsGuidePageState extends State { return Container(width: width.w, height: 1.w, color: Colors.white); } - Widget _buildRightCell(_CpRightItem? right) { - final opacity = (right?.unlocked ?? false) ? 1.0 : 0.55; - final title = right?.title.trim() ?? ""; - return Opacity( - opacity: opacity, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildRightIcon(right?.icon ?? ""), - if (title.isNotEmpty) ...[ - SizedBox(height: 4.w), - Padding( - padding: EdgeInsets.symmetric(horizontal: 6.w), - child: Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - textScaler: TextScaler.noScaling, - style: _bodyStyle(fontSize: 10, lineHeight: 1.1), - ), - ), - ], - ], - ), - ); - } - - Widget _buildRightIcon(String icon) { - if (icon.trim().isNotEmpty) { - return ClipRRect( - borderRadius: BorderRadius.circular(9.w), - child: netImage( - url: icon, - width: 42.w, - height: 42.w, - fit: BoxFit.cover, - noDefaultImg: true, - ), - ); - } - return Container( - width: 42.w, - height: 42.w, - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.18), - borderRadius: BorderRadius.circular(9.w), - border: Border.all( - color: Colors.white.withValues(alpha: 0.45), - width: 1.w, - ), - ), - ); - } - double _rightsIndicatorWidth(String label, TextStyle style) { final painter = TextPainter( text: TextSpan(text: label, style: style), @@ -1005,35 +921,8 @@ class _FaqItem extends StatelessWidget { } class _CpRightsLevel { - const _CpRightsLevel({ - required this.level, - required this.intimacy, - this.rights = const [], - }); + const _CpRightsLevel({required this.level, required this.intimacy}); final String level; final String intimacy; - final List<_CpRightItem> rights; - - int get rightSlots => rights.isEmpty ? 1 : rights.length; -} - -class _CpRightItem { - const _CpRightItem({ - required this.icon, - required this.title, - required this.unlocked, - }); - - factory _CpRightItem.fromRes(SCCpRightRes right) { - return _CpRightItem( - icon: right.icon?.trim() ?? "", - title: right.title?.trim() ?? "", - unlocked: right.unlocked ?? false, - ); - } - - final String icon; - final String title; - final bool unlocked; } diff --git a/lib/modules/gift/gift_page.dart b/lib/modules/gift/gift_page.dart index 0bedbbf..76eb819 100644 --- a/lib/modules/gift/gift_page.dart +++ b/lib/modules/gift/gift_page.dart @@ -21,6 +21,7 @@ import 'package:yumi/shared/data_sources/sources/remote/net/api.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart'; +import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart'; import 'package:yumi/main.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; @@ -34,6 +35,7 @@ import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/ui_kit/widgets/gift/sc_gift_combo_send_button.dart'; +import 'package:yumi/ui_kit/widgets/room/cp/room_cp_invite_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import 'package:yumi/modules/gift/gift_tab_page.dart'; import 'package:yumi/modules/gift/cp_rights/cp_rights_guide_page.dart'; @@ -1562,6 +1564,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { } await _refreshGiftBackpack(); _refreshCpStateAfterGift(request, profileManager); + _showCpGiftWaitingDialog(request); return; } @@ -1626,6 +1629,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { } profileManager?.updateBalance(result); _refreshCpStateAfterGift(request, profileManager); + _showCpGiftWaitingDialog(request); } catch (e) { final errorMessage = request.isBackpackGiftRequest && _isBackpackQuantityError(e) @@ -1668,6 +1672,73 @@ class _GiftPageState extends State with TickerProviderStateMixin { unawaited(profileManager?.refreshLoadedCpProfiles()); } + void _showCpGiftWaitingDialog(_GiftSendRequest request) { + if (!_isCpGift(request.gift)) { + return; + } + final context = navigatorKey.currentState?.context; + final currentProfile = AccountStorage().getCurrentUser()?.userProfile; + final receiverProfile = + request.acceptUsers.isNotEmpty ? request.acceptUsers.first.user : null; + if (context == null || currentProfile == null || receiverProfile == null) { + _giftFxLog( + 'skip local CP waiting dialog reason=missing_profile ' + 'giftId=${request.gift.id} ' + 'acceptUserIds=${request.acceptUserIds.join(",")}', + ); + return; + } + final relationType = scNormalizeCpRelationType(request.cpRelationType); + _giftFxLog( + 'show local CP waiting dialog ' + 'giftId=${request.gift.id} ' + 'relationType=$relationType ' + 'receiverId=${receiverProfile.id}', + ); + unawaited( + RoomCpInviteDialog.show( + context, + inviter: RoomCpInviteDialogUser( + name: _cpInviteDisplayName(currentProfile), + avatarUrl: currentProfile.userAvatar ?? "", + headdress: currentProfile.getHeaddress()?.sourceUrl ?? "", + headdressCover: currentProfile.getHeaddress()?.cover ?? "", + ), + receiver: RoomCpInviteDialogUser( + name: _cpInviteDisplayName(receiverProfile), + avatarUrl: receiverProfile.userAvatar ?? "", + headdress: receiverProfile.getHeaddress()?.sourceUrl ?? "", + headdressCover: receiverProfile.getHeaddress()?.cover ?? "", + ), + style: RoomCpInviteDialogStyle.waiting, + countdownSeconds: 86399, + descriptionText: _cpInviteWaitingDescriptionText(relationType), + ), + ); + } + + String _cpInviteDisplayName(SocialChatUserProfile profile) { + for (final value in [profile.userNickname, profile.account, profile.id]) { + final text = value?.trim() ?? ""; + if (text.isNotEmpty) { + return text; + } + } + return "User"; + } + + String _cpInviteWaitingDescriptionText(String relationType) { + switch (scNormalizeCpRelationType(relationType)) { + case "BROTHER": + return "We invite you to become Brother"; + case "SISTERS": + return "We invite you to become Sister"; + case "CP": + default: + return "We invite you to become Couple"; + } + } + Future> _giveBackpackGiftToRecipients( SCChatRoomRepository repository, _GiftSendRequest request, diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index 8df7f25..4beaeb9 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -295,6 +295,8 @@ class RealTimeMessagingManager extends ChangeNotifier { Timer? _roomRocketRoomReadyRewardRetryTimer; final Set _shownCpInviteMessageKeys = {}; final Set _shownCpInviteResultMessageKeys = {}; + final Set _shownCpInviteRejectedDialogKeys = {}; + final Map _recentCpRelationNoticeTimes = {}; int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken; String? roomRedPacketBroadcastGroupId; String? roomRedPacketBroadcastRegionCode; @@ -685,7 +687,10 @@ class RealTimeMessagingManager extends ChangeNotifier { }) async { final cpData = _decodeCpSystemMessageData(message); unawaited(_handleIncomingCpBuildMessage(message)); - final cpPeerUserId = _cpInviteConversationPeerIdFromData(cpData); + final cpPeerUserId = _cpInviteConversationPeerIdFromData( + cpData, + message: message, + ); final didSyncConversation = _upsertC2CConversationFromMessage( message, fallbackUserId: fallbackUserId ?? cpPeerUserId, @@ -816,7 +821,10 @@ class RealTimeMessagingManager extends ChangeNotifier { return ""; } - String? _cpInviteConversationPeerIdFromData(Map? data) { + String? _cpInviteConversationPeerIdFromData( + Map? data, { + V2TimMessage? message, + }) { if (data == null) { return null; } @@ -825,9 +833,27 @@ class RealTimeMessagingManager extends ChangeNotifier { if (invite == null || currentProfile == null) { return null; } - if (_cpInviteSenderMatchesCurrent(invite, currentProfile)) { + final currentIds = + [currentProfile.id, currentProfile.account] + .map((value) => value?.trim() ?? "") + .where((value) => value.isNotEmpty) + .toSet(); + final sender = message?.sender?.trim() ?? ""; + final userId = message?.userID?.trim() ?? ""; + final isSender = _cpInviteSenderMatchesCurrent( + invite, + currentProfile, + data: data, + ); + if (isSender) { + if (userId.isNotEmpty && !currentIds.contains(userId)) { + return userId; + } return _cpInviteReceiverIdFromMessageData(data); } + if (sender.isNotEmpty && !currentIds.contains(sender)) { + return sender; + } return _cpInviteInviterIdFromMessageData(data, invite); } @@ -908,7 +934,11 @@ class RealTimeMessagingManager extends ChangeNotifier { ); return; } - final isSender = _cpInviteSenderMatchesCurrent(invite, currentProfile); + final isSender = _cpInviteSenderMatchesCurrent( + invite, + currentProfile, + data: data, + ); final inviterName = _cpInviteDisplayName( nickname: invite.userNickname, account: invite.account, @@ -921,7 +951,11 @@ class RealTimeMessagingManager extends ChangeNotifier { ); final relationType = scNormalizeCpRelationType(invite.relationType); if (isSender) { - final receiver = _cpInviteReceiverFromMessageData(data); + final receiver = _cpInviteReceiverFromMessageData( + data, + fallbackName: _cpInviteReceiverFallbackName(data, message), + fallbackAvatar: _cpInviteReceiverFallbackAvatar(data, message), + ); _cpInviteLog( 'show waiting CP_BUILD applyId=$applyId ' 'relationType=$relationType', @@ -952,6 +986,11 @@ class RealTimeMessagingManager extends ChangeNotifier { 'relationType=$relationType ' 'inviter=$inviterName', ); + final currentReceiver = _cpInviteReceiverFromMessageData( + data, + fallbackName: _cpInviteReceiverFallbackName(data, null), + fallbackAvatar: _cpInviteReceiverFallbackAvatar(data, null), + ); await RoomCpInviteDialog.show( context, inviter: RoomCpInviteDialogUser( @@ -962,18 +1001,37 @@ class RealTimeMessagingManager extends ChangeNotifier { ), receiver: RoomCpInviteDialogUser( name: currentName, - avatarUrl: currentProfile.userAvatar ?? "", - headdress: currentProfile.getHeaddress()?.sourceUrl ?? "", - headdressCover: currentProfile.getHeaddress()?.cover ?? "", + avatarUrl: + _firstCpInviteValue([ + currentProfile.userAvatar, + currentReceiver.avatarUrl, + ]) ?? + "", + headdress: + _firstCpInviteValue([ + currentProfile.getHeaddress()?.sourceUrl, + currentReceiver.headdress, + ]) ?? + "", + headdressCover: + _firstCpInviteValue([ + currentProfile.getHeaddress()?.cover, + currentReceiver.headdressCover, + ]) ?? + "", ), style: RoomCpInviteDialogStyle.incoming, countdownSeconds: _cpInviteCountdownSeconds(invite), descriptionText: _cpInviteDescriptionText(relationType), onAccept: () { - unawaited(_processIncomingCpInvite(context, applyId, invite, true)); + unawaited( + _processIncomingCpInvite(context, applyId, invite, true, data: data), + ); }, onReject: () { - unawaited(_processIncomingCpInvite(context, applyId, invite, false)); + unawaited( + _processIncomingCpInvite(context, applyId, invite, false, data: data), + ); }, onClose: () { _cpInviteLog('close incoming CP_BUILD applyId=$applyId'); @@ -1066,6 +1124,29 @@ class RealTimeMessagingManager extends ChangeNotifier { } final currentProfile = AccountStorage().getCurrentUser()?.userProfile; if (context != null && invite != null && currentProfile != null) { + final isSender = _cpInviteSenderMatchesCurrent( + invite, + currentProfile, + data: data, + ); + if (result == _CpInviteProcessResult.rejected) { + if (isSender) { + _showCpInviteRejectedDialog(relationType, dedupKey: resultKey); + } + return true; + } + if (result == _CpInviteProcessResult.accepted) { + if (isSender) { + _dispatchCpRelationEstablishedRoomNotice( + groupId: _currentVisibleVoiceRoomGroupId(context), + applyId: applyId, + data: data, + invite: invite, + currentProfile: currentProfile, + ); + } + return true; + } unawaited( _showCpInviteResultDialog( context: context, @@ -1090,7 +1171,11 @@ class RealTimeMessagingManager extends ChangeNotifier { required _CpInviteProcessResult result, required String relationType, }) async { - final isSender = _cpInviteSenderMatchesCurrent(invite, currentProfile); + final isSender = _cpInviteSenderMatchesCurrent( + invite, + currentProfile, + data: data, + ); final currentName = _cpInviteDisplayName( nickname: currentProfile.userNickname, account: currentProfile.account, @@ -1105,6 +1190,7 @@ class RealTimeMessagingManager extends ChangeNotifier { 'show CP invite result dialog result=${result.name} ' 'relationType=$relationType isSender=$isSender', ); + final currentReceiver = _cpInviteReceiverFromMessageData(data); await RoomCpInviteDialog.show( context, inviter: RoomCpInviteDialogUser( @@ -1124,12 +1210,31 @@ class RealTimeMessagingManager extends ChangeNotifier { ), receiver: isSender - ? _cpInviteReceiverFromMessageData(data) + ? _cpInviteReceiverFromMessageData( + data, + fallbackName: _cpInviteReceiverFallbackName(data, null), + fallbackAvatar: _cpInviteReceiverFallbackAvatar(data, null), + ) : RoomCpInviteDialogUser( name: currentName, - avatarUrl: currentProfile.userAvatar ?? "", - headdress: currentProfile.getHeaddress()?.sourceUrl ?? "", - headdressCover: currentProfile.getHeaddress()?.cover ?? "", + avatarUrl: + _firstCpInviteValue([ + currentProfile.userAvatar, + currentReceiver.avatarUrl, + ]) ?? + "", + headdress: + _firstCpInviteValue([ + currentProfile.getHeaddress()?.sourceUrl, + currentReceiver.headdress, + ]) ?? + "", + headdressCover: + _firstCpInviteValue([ + currentProfile.getHeaddress()?.cover, + currentReceiver.headdressCover, + ]) ?? + "", ), style: isSender @@ -1271,13 +1376,15 @@ class RealTimeMessagingManager extends ChangeNotifier { BuildContext context, String applyId, SCSystemInvitMessageRes invite, - bool agree, - ) async { + bool agree, { + Map? data, + }) async { _cpInviteLog('process CP_BUILD applyId=$applyId agree=$agree'); final profileManager = Provider.of( context, listen: false, ); + final voiceRoomGroupId = _currentVisibleVoiceRoomGroupId(context); final success = await profileManager.cpRlationshipProcessApply( context, applyId, @@ -1295,6 +1402,23 @@ class RealTimeMessagingManager extends ChangeNotifier { ); SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag); unawaited(profileManager.refreshLoadedCpProfiles()); + if (agree) { + _dispatchCpRelationEstablishedRoomNotice( + groupId: voiceRoomGroupId, + applyId: applyId, + data: data, + invite: invite, + currentProfile: AccountStorage().getCurrentUser()?.userProfile, + ); + } else { + _dispatchCpInviteRejectedRoomNotice( + groupId: voiceRoomGroupId, + applyId: applyId, + data: data, + invite: invite, + currentProfile: AccountStorage().getCurrentUser()?.userProfile, + ); + } } Future sendCpRelationEstablishedSystemMessage({ @@ -1308,6 +1432,183 @@ class RealTimeMessagingManager extends ChangeNotifier { ); } + void _dispatchCpRelationEstablishedRoomNotice({ + required String groupId, + required String applyId, + required SCSystemInvitMessageRes invite, + required SocialChatUserProfile? currentProfile, + Map? data, + }) { + if (currentProfile == null) { + return; + } + if (groupId.isEmpty) { + _cpInviteLog( + 'skip room established notice reason=no_visible_voice_room ' + 'applyId=$applyId', + ); + return; + } + final isSender = _cpInviteSenderMatchesCurrent( + invite, + currentProfile, + data: data, + ); + final inviter = isSender ? currentProfile : _cpInviteInviterProfile(invite); + final receiver = isSender ? _cpInviteReceiverProfile(data) : currentProfile; + final relationType = scNormalizeCpRelationType( + _firstCpInviteValue([ + invite.relationType, + data == null ? null : data["relationType"]?.toString(), + data == null ? null : data["relation_type"]?.toString(), + ]) ?? + "", + ); + final targetUserIds = _compactCpNoticeTargetIds([inviter, receiver]); + _cpInviteLog( + 'dispatch room established notice applyId=$applyId ' + 'relationType=$relationType groupId=$groupId ' + 'isSender=$isSender targets=${targetUserIds.join(",")}', + ); + unawaited( + dispatchMessage( + Msg( + groupId: groupId, + msg: scBuildCpRelationEstablishedNotice( + userName1: _roomMsgUserDisplayName(inviter), + userName2: _roomMsgUserDisplayName(receiver), + relationType: relationType, + ), + type: SCRoomMsgType.cpSystemNotice, + noticeAction: "RELATION_ESTABLISHED", + user: receiver, + toUser: inviter, + targetUserIds: targetUserIds, + giftBatchId: applyId, + role: relationType, + ), + addLocal: true, + ), + ); + } + + void _dispatchCpInviteRejectedRoomNotice({ + required String groupId, + required String applyId, + required SCSystemInvitMessageRes invite, + required SocialChatUserProfile? currentProfile, + Map? data, + }) { + if (currentProfile == null) { + return; + } + if (groupId.isEmpty) { + _cpInviteLog( + 'skip room rejected notice reason=no_visible_voice_room ' + 'applyId=$applyId', + ); + return; + } + final inviter = _cpInviteInviterProfile(invite); + final targetUserIds = _compactCpNoticeTargetIds([inviter]); + final relationType = scNormalizeCpRelationType( + _firstCpInviteValue([ + invite.relationType, + data == null ? null : data["relationType"]?.toString(), + data == null ? null : data["relation_type"]?.toString(), + ]) ?? + "", + ); + _cpInviteLog( + 'dispatch room rejected notice applyId=$applyId ' + 'relationType=$relationType groupId=$groupId ' + 'targets=${targetUserIds.join(",")}', + ); + unawaited( + dispatchMessage( + Msg( + groupId: groupId, + msg: "", + type: SCRoomMsgType.cpSystemNotice, + noticeAction: "RELATION_REJECTED", + user: currentProfile, + toUser: inviter, + targetUserIds: targetUserIds, + giftBatchId: applyId, + role: relationType, + ), + addLocal: true, + ), + ); + } + + String _currentVisibleVoiceRoomGroupId(BuildContext context) { + final rtcProvider = Provider.of( + context, + listen: false, + ); + if (!rtcProvider.isVoiceRoomRouteVisible) { + return ""; + } + return (rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "") + .trim(); + } + + SocialChatUserProfile _cpInviteInviterProfile( + SCSystemInvitMessageRes invite, + ) { + final userId = + _firstCpInviteValue([invite.actualAccount, invite.account]) ?? ""; + return SocialChatUserProfile( + id: userId, + account: + _firstCpInviteValue([invite.account, invite.actualAccount]) ?? userId, + userNickname: _cpInviteDisplayName( + nickname: invite.userNickname, + account: invite.account, + actualAccount: invite.actualAccount, + fallback: userId, + ), + userAvatar: invite.userAvatar ?? "", + ); + } + + SocialChatUserProfile _cpInviteReceiverProfile(Map? data) { + final receiverData = + data == null + ? const {} + : Map.from(data); + final receiver = _cpInviteReceiverFromMessageData(receiverData); + final userId = _cpInviteReceiverIdFromMessageData(receiverData); + return SocialChatUserProfile( + id: userId, + account: + _firstCpInviteValue([ + receiverData["acceptAccount"]?.toString(), + receiverData["receiverAccount"]?.toString(), + userId, + ]) ?? + userId, + userNickname: receiver.name, + userAvatar: receiver.avatarUrl, + ); + } + + List _compactCpNoticeTargetIds( + Iterable profiles, + ) { + final result = []; + for (final profile in profiles) { + for (final value in [profile?.id, profile?.account]) { + final text = value?.trim() ?? ""; + if (text.isNotEmpty && !result.contains(text)) { + result.add(text); + } + } + } + return result; + } + Future sendCpInviteRejectedSystemMessage({ required String applyId, required SCSystemInvitMessageRes invite, @@ -1334,8 +1635,8 @@ class RealTimeMessagingManager extends ChangeNotifier { data["senderUserId"]?.toString(), data["senderId"]?.toString(), data["fromUserId"]?.toString(), - data["userId"]?.toString(), - content["actualAccount"]?.toString(), + data["applyUserId"]?.toString(), + data["account"]?.toString(), content["applyActualAccount"]?.toString(), content["applyUserActualAccount"]?.toString(), content["inviterActualAccount"]?.toString(), @@ -1344,11 +1645,14 @@ class RealTimeMessagingManager extends ChangeNotifier { content["senderUserId"]?.toString(), content["senderId"]?.toString(), content["fromUserId"]?.toString(), - content["userId"]?.toString(), - invite.actualAccount, - data["applyUserId"]?.toString(), content["applyUserId"]?.toString(), + content["applyUserAccount"]?.toString(), + invite.actualAccount, invite.account, + data["userId"]?.toString(), + content["actualAccount"]?.toString(), + content["userId"]?.toString(), + content["account"]?.toString(), ]) ?? ""; } @@ -1429,13 +1733,24 @@ class RealTimeMessagingManager extends ChangeNotifier { bool _cpInviteSenderMatchesCurrent( SCSystemInvitMessageRes invite, - SocialChatUserProfile currentProfile, - ) { + SocialChatUserProfile currentProfile, { + Map? data, + }) { final currentIds = [currentProfile.id, currentProfile.account] .map((value) => value?.trim() ?? "") .where((value) => value.isNotEmpty) .toSet(); + if (data != null) { + final receiverIds = _cpInviteReceiverIdentityValues(data); + if (currentIds.any(receiverIds.contains)) { + return false; + } + final explicitInviteIds = _cpInviteInviterIdentityValues(data); + if (explicitInviteIds.isNotEmpty) { + return currentIds.any(explicitInviteIds.contains); + } + } final inviteIds = [invite.actualAccount, invite.account] .map((value) => value?.trim() ?? "") @@ -1444,9 +1759,68 @@ class RealTimeMessagingManager extends ChangeNotifier { return currentIds.any(inviteIds.contains); } + Set _cpInviteInviterIdentityValues(Map data) { + final content = _cpInviteContentMap(data); + final values = [ + data["account"]?.toString(), + data["actualAccount"]?.toString(), + data["applyUserId"]?.toString(), + data["applyUserAccount"]?.toString(), + data["applyActualAccount"]?.toString(), + data["applyUserActualAccount"]?.toString(), + data["inviterActualAccount"]?.toString(), + data["inviterUserId"]?.toString(), + data["inviterId"]?.toString(), + data["senderUserId"]?.toString(), + data["senderId"]?.toString(), + data["fromUserId"]?.toString(), + content["applyUserId"]?.toString(), + content["applyUserAccount"]?.toString(), + content["applyActualAccount"]?.toString(), + content["applyUserActualAccount"]?.toString(), + content["inviterActualAccount"]?.toString(), + content["inviterUserId"]?.toString(), + content["inviterId"]?.toString(), + content["senderUserId"]?.toString(), + content["senderId"]?.toString(), + content["fromUserId"]?.toString(), + ]; + return values.map((value) => value?.trim() ?? "").where((value) { + return value.isNotEmpty; + }).toSet(); + } + + Set _cpInviteReceiverIdentityValues(Map data) { + final content = _cpInviteContentMap(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(); + } + RoomCpInviteDialogUser _cpInviteReceiverFromMessageData( - Map data, - ) { + Map data, { + String? fallbackName, + String? fallbackAvatar, + }) { final content = _cpInviteContentMap(data); final name = _cpInviteDisplayName( nickname: _firstCpInviteValue([ @@ -1472,6 +1846,7 @@ class RealTimeMessagingManager extends ChangeNotifier { content["receiverActualAccount"]?.toString(), ]), fallback: _firstCpInviteValue([ + fallbackName, data["acceptUserId"]?.toString(), data["receiverId"]?.toString(), data["receiverUserId"]?.toString(), @@ -1490,6 +1865,7 @@ class RealTimeMessagingManager extends ChangeNotifier { content["acceptUserAvatar"]?.toString(), content["receiverAvatar"]?.toString(), content["receiverUserAvatar"]?.toString(), + fallbackAvatar, ]) ?? "", headdress: @@ -1523,6 +1899,33 @@ class RealTimeMessagingManager extends ChangeNotifier { ); } + String? _cpInviteReceiverFallbackName( + Map data, + V2TimMessage? message, + ) { + final receiverId = _cpInviteReceiverIdFromMessageData(data); + final conversation = + receiverId.isNotEmpty ? conversationMap["c2c_$receiverId"] : null; + return _firstCpInviteValue([ + conversation?.showName, + message?.friendRemark, + message?.nickName, + message?.nameCard, + receiverId, + message?.userID, + ]); + } + + String? _cpInviteReceiverFallbackAvatar( + Map data, + V2TimMessage? message, + ) { + final receiverId = _cpInviteReceiverIdFromMessageData(data); + final conversation = + receiverId.isNotEmpty ? conversationMap["c2c_$receiverId"] : null; + return _firstCpInviteValue([conversation?.faceUrl, message?.faceUrl]); + } + String? _firstCpInviteValue(Iterable values) { for (final value in values) { final text = value?.trim() ?? ""; @@ -2065,9 +2468,16 @@ class RealTimeMessagingManager extends ChangeNotifier { } if (msg.type == SCRoomMsgType.cpSystemNotice && msg.noticeAction == "RELATION_REJECTED") { + if (_isDuplicateCpRelationNotice(msg)) { + return; + } _showCpInviteRejectedDialogIfNeeded(msg); return; } + if (msg.type == SCRoomMsgType.cpSystemNotice && + _isDuplicateCpRelationNotice(msg)) { + return; + } roomAllMsgList.insert(0, msg); if (roomAllMsgList.length > 250) { @@ -2168,6 +2578,69 @@ class RealTimeMessagingManager extends ChangeNotifier { ); } + bool _isDuplicateCpRelationNotice(Msg msg) { + if (msg.type != SCRoomMsgType.cpSystemNotice) { + return false; + } + final action = msg.noticeAction?.trim() ?? ""; + if (action != "RELATION_ESTABLISHED" && + action != "RELATION_JOINED_ROOM" && + action != "RELATION_REJECTED") { + return false; + } + final key = _cpRelationNoticeDedupKey(msg); + if (key.isEmpty) { + return false; + } + final now = DateTime.now().millisecondsSinceEpoch; + _recentCpRelationNoticeTimes.removeWhere((_, timestamp) { + return now - timestamp > 60000; + }); + final lastShownAt = _recentCpRelationNoticeTimes[key]; + if (lastShownAt != null && now - lastShownAt < 60000) { + _cpInviteLog('skip duplicated room CP notice key=$key'); + return true; + } + _recentCpRelationNoticeTimes[key] = now; + return false; + } + + String _cpRelationNoticeDedupKey(Msg msg) { + final explicitKey = msg.giftBatchId?.trim() ?? ""; + final action = msg.noticeAction?.trim() ?? ""; + final relationType = scNormalizeCpRelationType(msg.role); + final ids = + [_roomMsgUserId(msg.user), _roomMsgUserId(msg.toUser)] + ..removeWhere((value) => value.isEmpty) + ..sort(); + if (ids.isNotEmpty) { + return [ + "cp_notice", + action, + msg.groupId ?? "", + relationType, + ...ids, + ].join("|"); + } + if (explicitKey.isNotEmpty) { + return "cp_notice:$action:$explicitKey"; + } + if (ids.isEmpty) { + final names = [ + _roomMsgUserDisplayName(msg.user), + _roomMsgUserDisplayName(msg.toUser), + ]..sort(); + ids.addAll(names); + } + return [ + "cp_notice", + action, + msg.groupId ?? "", + relationType, + ...ids, + ].join("|"); + } + bool _isLiveCpRelationBroadcastMessage(Msg msg) { final timestamp = msg.time ?? 0; if (timestamp <= 0) { @@ -2203,8 +2676,7 @@ class RealTimeMessagingManager extends ChangeNotifier { return; } final currentProfile = AccountStorage().getCurrentUser()?.userProfile; - if (currentProfile == null || - _isSameRoomMsgUser(currentProfile, msg.user)) { + if (currentProfile == null) { return; } final targetIds = @@ -2214,6 +2686,7 @@ class RealTimeMessagingManager extends ChangeNotifier { .toSet() ?? const {}; final isTarget = + _isSameRoomMsgUser(currentProfile, msg.user) || _isSameRoomMsgUser(currentProfile, msg.toUser) || targetIds.contains(currentProfile.id?.trim() ?? "") || targetIds.contains(currentProfile.account?.trim() ?? ""); @@ -2250,10 +2723,26 @@ class RealTimeMessagingManager extends ChangeNotifier { return; } SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag); - _showCpInviteRejectedDialog(msg.role ?? ""); + _showCpInviteRejectedDialog( + msg.role ?? "", + dedupKey: _cpRelationRejectedDialogDedupKey(msg), + ); } - void _showCpInviteRejectedDialog(String relationType) { + String _cpRelationRejectedDialogDedupKey(Msg msg) { + final explicitKey = msg.giftBatchId?.trim() ?? ""; + if (explicitKey.isNotEmpty) { + return "rejected_$explicitKey"; + } + return _cpRelationNoticeDedupKey(msg); + } + + void _showCpInviteRejectedDialog(String relationType, {String? dedupKey}) { + final key = dedupKey?.trim() ?? ""; + if (key.isNotEmpty && !_shownCpInviteRejectedDialogKeys.add(key)) { + _cpInviteLog('skip duplicated rejected dialog key=$key'); + return; + } final context = navigatorKey.currentState?.context; if (context == null) { _cpInviteLog( diff --git a/lib/shared/business_logic/models/res/sc_system_invit_message_res.dart b/lib/shared/business_logic/models/res/sc_system_invit_message_res.dart index be3f43f..eac3ed0 100644 --- a/lib/shared/business_logic/models/res/sc_system_invit_message_res.dart +++ b/lib/shared/business_logic/models/res/sc_system_invit_message_res.dart @@ -166,8 +166,8 @@ class SCSystemInvitMessageRes { return null; } final source = { - ...root, if (content.isNotEmpty) ...content, + ...root, }; final relationType = _firstNonBlank(source, _relationTypeKeys) ?? diff --git a/lib/shared/data_sources/sources/local/floating_screen_manager.dart b/lib/shared/data_sources/sources/local/floating_screen_manager.dart index 1bcaa10..422b918 100644 --- a/lib/shared/data_sources/sources/local/floating_screen_manager.dart +++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart @@ -736,6 +736,7 @@ class OverlayManager { _removeMessagesByType(1); _removeMessagesByType(0); _removeMessagesByType(5); + _removeMessagesByType(6); if (normalizedRoomId.isNotEmpty) { _removeMessagesByType(3, roomId: normalizedRoomId); } @@ -871,6 +872,9 @@ class OverlayManager { if (message.type == 4) { return _isCurrentRoomFloatingMessage(context, message); } + if (message.type == 6) { + return _isCurrentRoomFloatingMessage(context, message); + } if (SCGlobalConfig.isFloatingBroadcastRoomOnly) { return _isCurrentRoomFloatingMessage(context, message); } @@ -895,11 +899,15 @@ class OverlayManager { } final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; + final currentRoomGroupId = + rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount?.trim() ?? + ""; final messageRoomId = (message.roomId ?? "").trim(); if (currentRoomId.isEmpty || messageRoomId.isEmpty) { return false; } - return currentRoomId == messageRoomId; + return currentRoomId == messageRoomId || + (currentRoomGroupId.isNotEmpty && currentRoomGroupId == messageRoomId); } bool _shouldDisplayRegionBroadcastMessage( diff --git a/lib/ui_kit/widgets/msg/message_conversation_list_page.dart b/lib/ui_kit/widgets/msg/message_conversation_list_page.dart index 1852592..848d30f 100644 --- a/lib/ui_kit/widgets/msg/message_conversation_list_page.dart +++ b/lib/ui_kit/widgets/msg/message_conversation_list_page.dart @@ -346,7 +346,7 @@ class _MessageConversationListPageState } _loadingConversationUserIds.add(userId); SCAccountRepository() - .loadUserInfo(userId) + .loadUserInfo(userId, silentErrorToast: true) .then((value) { if (!mounted) return; setState(() { @@ -411,7 +411,14 @@ class _ConversationItemState extends State { _effectiveUserId.isNotEmpty; String get _effectiveUserId { - final cpInvitePeerId = _cpInvitePeerUserIdFromLastMessage(); + final cpInviteProfile = _cpInvitePeerProfileFromLastMessage(); + final cpInvitePeerId = + _firstCpInviteValue([ + _cpInvitePeerUserIdFromLastMessage(), + cpInviteProfile?.id, + cpInviteProfile?.account, + ]) ?? + ""; if (cpInvitePeerId.isNotEmpty) { return cpInvitePeerId; } @@ -423,6 +430,11 @@ class _ConversationItemState extends State { if (userAvatar.isNotEmpty) { return userAvatar; } + final cpInviteAvatar = + _cpInvitePeerProfileFromLastMessage()?.userAvatar ?? ""; + if (cpInviteAvatar.isNotEmpty) { + return cpInviteAvatar; + } return conversation.faceUrl ?? ""; } @@ -431,6 +443,17 @@ class _ConversationItemState extends State { if (nickname.isNotEmpty) { return nickname; } + final cpInviteProfile = _cpInvitePeerProfileFromLastMessage(); + final cpInviteName = + _firstCpInviteValue([ + cpInviteProfile?.userNickname, + cpInviteProfile?.id, + cpInviteProfile?.account, + ]) ?? + ""; + if (cpInviteName.isNotEmpty) { + return cpInviteName; + } return conversation.showName ?? conversation.userID ?? conversation.groupID ?? @@ -439,7 +462,7 @@ class _ConversationItemState extends State { @override void initState() { - user = widget.initialUser; + user = widget.initialUser ?? _cpInvitePeerProfileFromLastMessage(); loadUserInfo(); super.initState(); } @@ -449,6 +472,11 @@ class _ConversationItemState extends State { super.didUpdateWidget(oldWidget); if (widget.initialUser != null && widget.initialUser != user) { user = widget.initialUser; + return; + } + final cpInviteProfile = _cpInvitePeerProfileFromLastMessage(); + if (user == null && cpInviteProfile != null) { + user = cpInviteProfile; } } @@ -672,26 +700,50 @@ class _ConversationItemState extends State { } void loadUserInfo() { - if (!_shouldLoadUserInfo || user != null) return; + if (!_shouldLoadUserInfo) return; final userId = _effectiveUserId; if (userId.isEmpty) return; - SCAccountRepository().loadUserInfo(userId).then((value) { - if (!mounted) return; - user = value; - setState(() {}); - }); + SCAccountRepository() + .loadUserInfo(userId, silentErrorToast: true) + .then((value) { + if (!mounted) return; + user = value; + setState(() {}); + }) + .catchError((_) { + if (!mounted) return; + user ??= _cpInvitePeerProfileFromLastMessage(); + setState(() {}); + }); } V2TimConversation _routeConversation() { final peerId = _effectiveUserId; - if (!_isC2CConversation || - peerId.isEmpty || - peerId == (conversation.userID?.trim() ?? "")) { + if (!_isC2CConversation || peerId.isEmpty) { return conversation; } final json = conversation.toJson(); - json["userID"] = peerId; - json["conversationID"] = "c2c_$peerId"; + final isSamePeer = peerId == (conversation.userID?.trim() ?? ""); + if (!isSamePeer) { + json["userID"] = peerId; + json["conversationID"] = "c2c_$peerId"; + } + final profile = _cpInvitePeerProfileFromLastMessage(); + final profileName = _firstCpInviteValue([ + profile?.userNickname, + profile?.id, + profile?.account, + ]); + if ((profileName ?? "").isNotEmpty) { + json["showName"] = profileName; + } + final profileAvatar = profile?.userAvatar?.trim() ?? ""; + if (profileAvatar.isNotEmpty) { + json["faceUrl"] = profileAvatar; + } + if (isSamePeer && profileName == null && profileAvatar.isEmpty) { + return conversation; + } return V2TimConversation.fromJson(json); } @@ -699,10 +751,73 @@ class _ConversationItemState extends State { if (!_isC2CConversation) { return ""; } + final message = conversation.lastMessage; final data = _cpInviteCustomData(conversation.lastMessage); - if (data == null) { + if (data == null || !_isCpBuildInviteData(data)) { return ""; } + final currentProfile = AccountStorage().getCurrentUser()?.userProfile; + final currentIds = + [currentProfile?.id, currentProfile?.account] + .map((value) => value?.trim() ?? "") + .where((value) => value.isNotEmpty) + .toSet(); + final isCurrentInviter = _cpInviteCurrentUserIsInviter(data); + final peerProfile = _cpInvitePeerProfileFromData(data); + final sender = message?.sender?.trim() ?? ""; + final messageUserId = + _firstCpInviteValue([message?.userID, conversation.userID]) ?? ""; + if (isCurrentInviter) { + return _firstCpInviteValue([ + peerProfile?.id, + currentIds.contains(messageUserId) ? null : messageUserId, + _cpInviteReceiverIdFromData(data), + peerProfile?.account, + ]) ?? + ""; + } + return _firstCpInviteValue([ + currentIds.contains(sender) ? null : sender, + peerProfile?.id, + _cpInviteInviterIdFromData(data), + peerProfile?.account, + ]) ?? + ""; + } + + SocialChatUserProfile? _cpInvitePeerProfileFromLastMessage() { + final data = _cpInviteCustomData(conversation.lastMessage); + if (data == null || !_isCpBuildInviteData(data)) { + return null; + } + return _cpInvitePeerProfileFromData(data); + } + + SocialChatUserProfile? _cpInvitePeerProfileFromData( + Map data, + ) { + final profile = + _cpInviteCurrentUserIsInviter(data) + ? _cpInviteReceiverProfileFromData(data) + : _cpInviteInviterProfileFromData(data); + final currentProfile = AccountStorage().getCurrentUser()?.userProfile; + final currentIds = + [currentProfile?.id, currentProfile?.account] + .map((value) => value?.trim() ?? "") + .where((value) => value.isNotEmpty) + .toSet(); + final profileIds = + [profile?.id, profile?.account] + .map((value) => value?.trim() ?? "") + .where((value) => value.isNotEmpty) + .toSet(); + if (currentIds.any(profileIds.contains)) { + return null; + } + return profile; + } + + bool _isCpBuildInviteData(Map data) { final content = _cpInviteContentMap(data); final noticeType = _firstCpInviteValue([ @@ -712,17 +827,208 @@ class _ConversationItemState extends State { content["type"]?.toString(), ])?.toUpperCase() ?? ""; - if (!noticeType.contains(SCSysytemMessageType.CP_BUILD.name)) { - return ""; - } + return noticeType.contains(SCSysytemMessageType.CP_BUILD.name); + } + + bool _cpInviteCurrentUserIsInviter(Map data) { final currentProfile = AccountStorage().getCurrentUser()?.userProfile; + if (currentProfile == null) { + return false; + } final currentIds = - [currentProfile?.id, currentProfile?.account] + [currentProfile.id, currentProfile.account] .map((value) => value?.trim() ?? "") .where((value) => value.isNotEmpty) .toSet(); - final inviterIds = _compactCpInviteValues([ + 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); + } + + SocialChatUserProfile? _cpInviteInviterProfileFromData( + Map data, + ) { + final content = _cpInviteContentMap(data); + final nested = _firstCpInviteMap(data, content, const [ + "applyUser", + "applyUserProfile", + "inviter", + "inviterProfile", + "sender", + "senderProfile", + "fromUser", + "fromUserProfile", + "user", + "userProfile", + "profile", + ]); + return _cpInviteProfileOrNull( + id: _firstCpInviteValue([ + nested["id"]?.toString(), + nested["userId"]?.toString(), + nested["actualAccount"]?.toString(), + data["actualAccount"]?.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(), + data["applyUserId"]?.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(), + content["applyUserId"]?.toString(), + ]), + account: _firstCpInviteValue([ + nested["account"]?.toString(), + data["account"]?.toString(), + data["applyUserAccount"]?.toString(), + content["applyUserAccount"]?.toString(), + ]), + nickname: _firstCpInviteValue([ + nested["userNickname"]?.toString(), + nested["nickname"]?.toString(), + data["applyNickname"]?.toString(), + data["applyUserNickname"]?.toString(), + data["inviterNickname"]?.toString(), + data["senderNickname"]?.toString(), + data["userNickname"]?.toString(), + content["applyNickname"]?.toString(), + content["applyUserNickname"]?.toString(), + content["inviterNickname"]?.toString(), + content["senderNickname"]?.toString(), + ]), + avatar: _firstCpInviteValue([ + nested["userAvatar"]?.toString(), + nested["avatar"]?.toString(), + data["applyUserAvatar"]?.toString(), + data["inviterAvatar"]?.toString(), + data["senderAvatar"]?.toString(), + data["userAvatar"]?.toString(), + content["applyUserAvatar"]?.toString(), + content["inviterAvatar"]?.toString(), + content["senderAvatar"]?.toString(), + ]), + ); + } + + SocialChatUserProfile? _cpInviteReceiverProfileFromData( + Map data, + ) { + final content = _cpInviteContentMap(data); + final nested = _firstCpInviteMap(data, content, const [ + "acceptUser", + "acceptUserProfile", + "receiver", + "receiverProfile", + "toUser", + "toUserProfile", + "targetUser", + "targetUserProfile", + ]); + return _cpInviteProfileOrNull( + id: _firstCpInviteValue([ + nested["id"]?.toString(), + nested["userId"]?.toString(), + nested["actualAccount"]?.toString(), + data["acceptActualAccount"]?.toString(), + data["acceptUserId"]?.toString(), + data["receiverActualAccount"]?.toString(), + data["receiverId"]?.toString(), + data["receiverUserId"]?.toString(), + data["targetUserId"]?.toString(), + data["toUserId"]?.toString(), + content["acceptActualAccount"]?.toString(), + content["acceptUserId"]?.toString(), + content["receiverActualAccount"]?.toString(), + content["receiverId"]?.toString(), + content["receiverUserId"]?.toString(), + content["targetUserId"]?.toString(), + content["toUserId"]?.toString(), + ]), + account: _firstCpInviteValue([ + nested["account"]?.toString(), + data["acceptAccount"]?.toString(), + data["receiverAccount"]?.toString(), + content["acceptAccount"]?.toString(), + content["receiverAccount"]?.toString(), + ]), + nickname: _firstCpInviteValue([ + nested["userNickname"]?.toString(), + nested["nickname"]?.toString(), + data["acceptNickname"]?.toString(), + data["acceptUserNickname"]?.toString(), + data["receiverNickname"]?.toString(), + data["receiverUserNickname"]?.toString(), + content["acceptNickname"]?.toString(), + content["acceptUserNickname"]?.toString(), + content["receiverNickname"]?.toString(), + content["receiverUserNickname"]?.toString(), + ]), + avatar: _firstCpInviteValue([ + nested["userAvatar"]?.toString(), + nested["avatar"]?.toString(), + data["acceptUserAvatar"]?.toString(), + data["receiverAvatar"]?.toString(), + data["receiverUserAvatar"]?.toString(), + content["acceptUserAvatar"]?.toString(), + content["receiverAvatar"]?.toString(), + content["receiverUserAvatar"]?.toString(), + ]), + ); + } + + SocialChatUserProfile? _cpInviteProfileOrNull({ + String? id, + String? account, + String? nickname, + String? avatar, + }) { + final hasValue = [id, account, nickname, avatar].any((value) { + return (value?.trim() ?? "").isNotEmpty; + }); + if (!hasValue) { + return null; + } + return SocialChatUserProfile( + id: id?.trim(), + account: account?.trim(), + userNickname: nickname?.trim(), + userAvatar: avatar?.trim(), + ); + } + + String _cpInviteInviterIdFromData(Map data) { + final profile = _cpInviteInviterProfileFromData(data); + return _firstCpInviteValue([profile?.id, profile?.account]) ?? ""; + } + + String _cpInviteReceiverIdFromData(Map data) { + final profile = _cpInviteReceiverProfileFromData(data); + return _firstCpInviteValue([profile?.id, profile?.account]) ?? ""; + } + + Set _cpInviteInviterIdentityValues(Map data) { + final content = _cpInviteContentMap(data); + return _compactCpInviteValues([ + data["account"]?.toString(), data["actualAccount"]?.toString(), + data["applyUserId"]?.toString(), + data["applyUserAccount"]?.toString(), data["applyActualAccount"]?.toString(), data["applyUserActualAccount"]?.toString(), data["inviterActualAccount"]?.toString(), @@ -731,8 +1037,8 @@ class _ConversationItemState extends State { data["senderUserId"]?.toString(), data["senderId"]?.toString(), data["fromUserId"]?.toString(), - data["userId"]?.toString(), - content["actualAccount"]?.toString(), + content["applyUserId"]?.toString(), + content["applyUserAccount"]?.toString(), content["applyActualAccount"]?.toString(), content["applyUserActualAccount"]?.toString(), content["inviterActualAccount"]?.toString(), @@ -741,29 +1047,67 @@ class _ConversationItemState extends State { content["senderUserId"]?.toString(), content["senderId"]?.toString(), content["fromUserId"]?.toString(), - content["userId"]?.toString(), - data["applyUserId"]?.toString(), - content["applyUserId"]?.toString(), - data["account"]?.toString(), - content["account"]?.toString(), - ]); - final isCurrentInviter = currentIds.any(inviterIds.contains); - final values = - isCurrentInviter - ? [ - data["acceptActualAccount"]?.toString(), - data["acceptUserId"]?.toString(), - data["receiverActualAccount"]?.toString(), - data["receiverId"]?.toString(), - data["receiverUserId"]?.toString(), - content["acceptActualAccount"]?.toString(), - content["acceptUserId"]?.toString(), - content["receiverActualAccount"]?.toString(), - content["receiverId"]?.toString(), - content["receiverUserId"]?.toString(), - ] - : inviterIds; - return _firstCpInviteValue(values) ?? ""; + ]).toSet(); + } + + Set _cpInviteReceiverIdentityValues(Map data) { + final content = _cpInviteContentMap(data); + return _compactCpInviteValues([ + 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(), + ]).toSet(); + } + + Map _firstCpInviteMap( + Map data, + Map content, + Iterable keys, + ) { + for (final key in keys) { + final map = _cpInviteStringKeyMap(data[key]); + if (map.isNotEmpty) { + return map; + } + final contentMap = _cpInviteStringKeyMap(content[key]); + if (contentMap.isNotEmpty) { + return contentMap; + } + } + return const {}; + } + + Map _cpInviteStringKeyMap(dynamic value) { + if (value is Map) { + return value; + } + if (value is Map) { + return value.map((key, item) => MapEntry(key.toString(), item)); + } + if (value is String && value.trim().isNotEmpty) { + try { + final decoded = jsonDecode(value); + if (decoded is Map) { + return decoded.map((key, item) => MapEntry(key.toString(), item)); + } + } catch (_) {} + } + return const {}; } Map? _cpInviteCustomData(V2TimMessage? message) { diff --git a/test/gift_res_cp_relation_type_test.dart b/test/gift_res_cp_relation_type_test.dart index f31f8ce..14e3c2a 100644 --- a/test/gift_res_cp_relation_type_test.dart +++ b/test/gift_res_cp_relation_type_test.dart @@ -108,6 +108,22 @@ void main() { expect(invite?.actualAccount, '2057731345099841537'); }); + test('keeps top-level applicant when content carries receiver profile', () { + final invite = SCSystemInvitMessageRes.fromMessageData({ + 'account': '99004', + 'actualAccount': 'sender-long-id', + 'relationType': 'CP', + 'content': jsonEncode({ + 'account': '99005', + 'actualAccount': 'receiver-long-id', + 'userNickname': 'Receiver', + }), + }); + + expect(invite?.account, '99004'); + expect(invite?.actualAccount, 'sender-long-id'); + }); + test('infers relation type from gift payload when needed', () { final invite = SCSystemInvitMessageRes.fromMessageData({ 'content': {