飘屏动画

This commit is contained in:
roxy 2026-05-23 10:08:54 +08:00
parent cbb811a18a
commit 71993f438b
8 changed files with 1199 additions and 255 deletions

View File

@ -368,7 +368,7 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
}
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<SCMessageChatPage> {
(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<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 inviterIds =
<String>[
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 = <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) {

View File

@ -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<CpRightsGuidePage> {
? "Lv.$levelNumber"
: "Lv.${rows.length + 1}"),
intimacy: _formatRightsIntimacyValue(requiredValue),
rights: level.rights.map(_CpRightItem.fromRes).toList(),
),
);
}
@ -630,23 +628,16 @@ class _CpRightsGuidePageState extends State<CpRightsGuidePage> {
}
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<double>(
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<CpRightsGuidePage> {
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<CpRightsGuidePage> {
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<CpRightsGuidePage> {
);
}
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<CpRightsGuidePage> {
}
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<CpRightsGuidePage> {
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<CpRightsGuidePage> {
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;
}

View File

@ -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<GiftPage> with TickerProviderStateMixin {
}
await _refreshGiftBackpack();
_refreshCpStateAfterGift(request, profileManager);
_showCpGiftWaitingDialog(request);
return;
}
@ -1626,6 +1629,7 @@ class _GiftPageState extends State<GiftPage> 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<GiftPage> 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<List<MicRes>> _giveBackpackGiftToRecipients(
SCChatRoomRepository repository,
_GiftSendRequest request,

View File

@ -295,6 +295,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
Timer? _roomRocketRoomReadyRewardRetryTimer;
final Set<String> _shownCpInviteMessageKeys = <String>{};
final Set<String> _shownCpInviteResultMessageKeys = <String>{};
final Set<String> _shownCpInviteRejectedDialogKeys = <String>{};
final Map<String, int> _recentCpRelationNoticeTimes = <String, int>{};
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<String, dynamic>? data) {
String? _cpInviteConversationPeerIdFromData(
Map<String, dynamic>? 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<String, dynamic>? data,
}) async {
_cpInviteLog('process CP_BUILD applyId=$applyId agree=$agree');
final profileManager = Provider.of<SocialChatUserProfileManager>(
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<void> sendCpRelationEstablishedSystemMessage({
@ -1308,6 +1432,183 @@ class RealTimeMessagingManager extends ChangeNotifier {
);
}
void _dispatchCpRelationEstablishedRoomNotice({
required String groupId,
required String applyId,
required SCSystemInvitMessageRes invite,
required SocialChatUserProfile? currentProfile,
Map<String, dynamic>? 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<String, dynamic>? 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<RealTimeCommunicationManager>(
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<String, dynamic>? data) {
final receiverData =
data == null
? const <String, dynamic>{}
: Map<String, dynamic>.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<String> _compactCpNoticeTargetIds(
Iterable<SocialChatUserProfile?> profiles,
) {
final result = <String>[];
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<void> 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<String, dynamic>? 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<String> _cpInviteInviterIdentityValues(Map<String, dynamic> data) {
final content = _cpInviteContentMap(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(),
];
return values.map((value) => value?.trim() ?? "").where((value) {
return value.isNotEmpty;
}).toSet();
}
Set<String> _cpInviteReceiverIdentityValues(Map<String, dynamic> 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<String, dynamic> data,
) {
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> data,
V2TimMessage? message,
) {
final receiverId = _cpInviteReceiverIdFromMessageData(data);
final conversation =
receiverId.isNotEmpty ? conversationMap["c2c_$receiverId"] : null;
return _firstCpInviteValue([conversation?.faceUrl, message?.faceUrl]);
}
String? _firstCpInviteValue(Iterable<String?> 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 <String>{};
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(

View File

@ -166,8 +166,8 @@ class SCSystemInvitMessageRes {
return null;
}
final source = <String, dynamic>{
...root,
if (content.isNotEmpty) ...content,
...root,
};
final relationType =
_firstNonBlank(source, _relationTypeKeys) ??

View File

@ -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(

View File

@ -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<ConversationItem> {
_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<ConversationItem> {
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<ConversationItem> {
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<ConversationItem> {
@override
void initState() {
user = widget.initialUser;
user = widget.initialUser ?? _cpInvitePeerProfileFromLastMessage();
loadUserInfo();
super.initState();
}
@ -449,6 +472,11 @@ class _ConversationItemState extends State<ConversationItem> {
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<ConversationItem> {
}
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<ConversationItem> {
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 =
<String?>[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<String, dynamic> 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<String, dynamic> data) {
final content = _cpInviteContentMap(data);
final noticeType =
_firstCpInviteValue([
@ -712,17 +827,208 @@ class _ConversationItemState extends State<ConversationItem> {
content["type"]?.toString(),
])?.toUpperCase() ??
"";
if (!noticeType.contains(SCSysytemMessageType.CP_BUILD.name)) {
return "";
}
return noticeType.contains(SCSysytemMessageType.CP_BUILD.name);
}
bool _cpInviteCurrentUserIsInviter(Map<String, dynamic> data) {
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
if (currentProfile == null) {
return false;
}
final currentIds =
<String?>[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<String, dynamic> 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<String, dynamic> 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<String, dynamic> data) {
final profile = _cpInviteInviterProfileFromData(data);
return _firstCpInviteValue([profile?.id, profile?.account]) ?? "";
}
String _cpInviteReceiverIdFromData(Map<String, dynamic> data) {
final profile = _cpInviteReceiverProfileFromData(data);
return _firstCpInviteValue([profile?.id, profile?.account]) ?? "";
}
Set<String> _cpInviteInviterIdentityValues(Map<String, dynamic> 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<ConversationItem> {
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<ConversationItem> {
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<String> _cpInviteReceiverIdentityValues(Map<String, dynamic> 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<String, dynamic> _firstCpInviteMap(
Map<String, dynamic> data,
Map<String, dynamic> content,
Iterable<String> 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 <String, dynamic>{};
}
Map<String, dynamic> _cpInviteStringKeyMap(dynamic value) {
if (value is Map<String, dynamic>) {
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 <String, dynamic>{};
}
Map<String, dynamic>? _cpInviteCustomData(V2TimMessage? message) {

View File

@ -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': {