修改消息和rtc

This commit is contained in:
zhx 2026-05-23 19:08:07 +08:00
parent 1e7261927e
commit 8f95b72fd0
4 changed files with 510 additions and 21 deletions

View File

@ -1224,6 +1224,11 @@ class _MessageItem extends StatelessWidget {
),
);
}
final customData = _customDataFromMessage(message);
if (customData != null &&
_isDuplicateCpResultMessageForConversation(customData)) {
return const SizedBox.shrink();
}
final cpRelationFormedCard = _cpRelationFormedMessageCard();
final cpStatusLineText = _cpStatusLineTextForMessage();
return Container(
@ -1381,7 +1386,7 @@ class _MessageItem extends StatelessWidget {
return GestureDetector(
onLongPress: () => _showMsgItemMenu(ct, ""),
onTap:
() => _showCpInviteDialogFromMessage(data, cpInvite, actionState),
() => _openCpInviteDialogFromMessage(data, cpInvite, actionState),
child: Container(
width: 251.w,
height: 64.w,
@ -1451,6 +1456,30 @@ class _MessageItem extends StatelessWidget {
);
}
void _openCpInviteDialogFromMessage(
Map<String, dynamic> data,
SCSystemInvitMessageRes? cpInvite,
RoomCpInviteActionState actionState,
) {
unawaited(_openCpInviteDialogFromMessageAsync(data, cpInvite, actionState));
}
Future<void> _openCpInviteDialogFromMessageAsync(
Map<String, dynamic> data,
SCSystemInvitMessageRes? cpInvite,
RoomCpInviteActionState actionState,
) async {
final latestState = await _latestCpInviteStateForMessage(data);
if (!context.mounted) {
return;
}
final resolvedActionState =
latestState == _CpInviteMessageState.pending
? actionState
: _cpInviteActionState(latestState);
_showCpInviteDialogFromMessage(data, cpInvite, resolvedActionState);
}
Widget _buildCpInviteGiftThumb(String giftCover) {
final child =
giftCover.isNotEmpty
@ -1623,12 +1652,22 @@ class _MessageItem extends StatelessWidget {
return null;
}
final localizations = SCAppLocalizations.of(context)!;
final relationType = _cpRelationTypeFromMessageData(data);
if (state == _CpInviteMessageState.expired) {
if (relationType != "CP") {
return "The ${scCpRelationPossessiveLabel(relationType)} invitation timed out.";
}
return localizations.closeFriendInviteTimedOut;
}
if (relationType == "CP") {
return _isCurrentUserCpInviter(data)
? localizations.otherDeclinedCloseFriendInvite
: localizations.youDeclinedCloseFriendInvite;
}
final relationLabel = scCpRelationPossessiveLabel(relationType);
return _isCurrentUserCpInviter(data)
? localizations.otherDeclinedCloseFriendInvite
: localizations.youDeclinedCloseFriendInvite;
? "The other party declined your $relationLabel invitation."
: "You declined the other party's $relationLabel invitation.";
}
void _showCpInviteDialogFromMessage(
@ -1782,14 +1821,15 @@ class _MessageItem extends StatelessWidget {
cpInvite != null &&
actionState == RoomCpInviteActionState.pending &&
applyId.isNotEmpty
? () => _processCpInviteFromMessage(applyId, cpInvite, true)
? () => _processCpInviteFromMessage(applyId, cpInvite, true, data)
: null,
onReject:
!isInviter &&
cpInvite != null &&
actionState == RoomCpInviteActionState.pending &&
applyId.isNotEmpty
? () => _processCpInviteFromMessage(applyId, cpInvite, false)
? () =>
_processCpInviteFromMessage(applyId, cpInvite, false, data)
: null,
);
}
@ -1798,16 +1838,37 @@ class _MessageItem extends StatelessWidget {
String applyId,
SCSystemInvitMessageRes invite,
bool agree,
Map<String, dynamic> data,
) {
unawaited(_processCpInviteFromMessageAsync(applyId, invite, agree));
unawaited(_processCpInviteFromMessageAsync(applyId, invite, agree, data));
}
Future<void> _processCpInviteFromMessageAsync(
String applyId,
SCSystemInvitMessageRes invite,
bool agree,
Map<String, dynamic> data,
) async {
_cpMessageLog('process applyId=$applyId agree=$agree');
final latestState = await _latestCpInviteStateForMessage(data);
if (latestState != _CpInviteMessageState.pending) {
_cpMessageLog(
'skip process applyId=$applyId agree=$agree latestState=$latestState',
);
if (!context.mounted) {
return;
}
_showCpInviteDialogFromMessage(
data,
invite,
_cpInviteActionState(latestState),
);
updateCall();
return;
}
if (!context.mounted) {
return;
}
final profileManager = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
@ -1821,6 +1882,9 @@ class _MessageItem extends StatelessWidget {
_cpMessageLog('process failed applyId=$applyId agree=$agree');
return;
}
if (!context.mounted) {
return;
}
SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag);
_cpMessageLog(
'process submitted applyId=$applyId agree=$agree '
@ -1830,6 +1894,58 @@ class _MessageItem extends StatelessWidget {
updateCall();
}
Future<_CpInviteMessageState> _latestCpInviteStateForMessage(
Map<String, dynamic> data,
) async {
final localState = _cpInviteStateForMessage(data);
if (localState != _CpInviteMessageState.pending) {
return localState;
}
final applyId = _cpApplyIdFromData(data);
if (applyId.isEmpty) {
return localState;
}
try {
final status = await SCAccountRepository().cpRelationshipApplyStatus(
applyId,
);
final backendState = _cpInviteStateFromBackendStatus(status);
if (backendState != null) {
return backendState;
}
} catch (error) {
_cpMessageLog('query apply status failed applyId=$applyId error=$error');
}
return _cpInviteSecondsLeft(data) <= 0
? _CpInviteMessageState.expired
: localState;
}
_CpInviteMessageState? _cpInviteStateFromBackendStatus(String? status) {
final text = status?.trim().toUpperCase() ?? "";
if (text.isEmpty || text == "WAIT" || text == "WAITING") {
return text.isEmpty ? null : _CpInviteMessageState.pending;
}
if (text.contains("EXPIRE") ||
text.contains("TIMEOUT") ||
text.contains("OVERDUE")) {
return _CpInviteMessageState.expired;
}
if (text.contains("REFUSE") ||
text.contains("REJECT") ||
text.contains("DECLIN") ||
text.contains("DENY")) {
return _CpInviteMessageState.rejected;
}
if (text.contains("AGREE") ||
text.contains("ACCEPT") ||
text.contains("APPROVE") ||
text.contains("ESTABLISH")) {
return _CpInviteMessageState.accepted;
}
return null;
}
Map<String, dynamic>? _customDataFromMessage(V2TimMessage msg) {
if (msg.elemType != MessageElemType.V2TIM_ELEM_TYPE_CUSTOM) {
return null;
@ -1891,6 +2007,60 @@ class _MessageItem extends StatelessWidget {
return _CpInviteMessageState.pending;
}
bool _isDuplicateCpResultMessageForConversation(Map<String, dynamic> data) {
final currentKey = _cpResultMessageDedupKey(data);
if (currentKey.isEmpty) {
return false;
}
final currentIndex = currentConversationMessageList.indexWhere(
_isCurrentConversationMessage,
);
if (currentIndex <= 0) {
return false;
}
for (var i = 0; i < currentIndex; i++) {
final otherData = _customDataFromMessage(
currentConversationMessageList[i],
);
if (otherData == null) {
continue;
}
if (_cpResultMessageDedupKey(otherData) == currentKey) {
return true;
}
}
return false;
}
bool _isCurrentConversationMessage(V2TimMessage item) {
final currentMsgId = message.msgID?.trim() ?? "";
final itemMsgId = item.msgID?.trim() ?? "";
if (currentMsgId.isNotEmpty && itemMsgId.isNotEmpty) {
return currentMsgId == itemMsgId;
}
final currentSeq = message.seq?.trim() ?? "";
final itemSeq = item.seq?.trim() ?? "";
if (currentSeq.isNotEmpty && itemSeq.isNotEmpty) {
return currentSeq == itemSeq && item.timestamp == message.timestamp;
}
return identical(item, message);
}
String _cpResultMessageDedupKey(Map<String, dynamic> data) {
if (!_isCpInvitePayload(data)) {
return "";
}
final state = _cpInviteStateFromData(data);
if (state == _CpInviteMessageState.pending) {
return "";
}
final applyId = _cpApplyIdFromData(data);
if (applyId.isEmpty) {
return "";
}
return "$applyId|${state.name}";
}
_CpInviteMessageState _cpInviteStateFromData(Map<String, dynamic> data) {
final content = _cpContentMap(data);
final values = <String>[
@ -2145,6 +2315,20 @@ class _MessageItem extends StatelessWidget {
], fallback: "");
}
String _cpRelationTypeFromMessageData(Map<String, dynamic> data) {
final invite = _cpInviteFromMessageData(data);
final content = _cpContentMap(data);
return scNormalizeCpRelationType(
_firstNonBlankString([
invite?.relationType,
data["relationType"]?.toString(),
data["relation_type"]?.toString(),
content["relationType"]?.toString(),
content["relation_type"]?.toString(),
], fallback: "CP"),
);
}
Map<String, dynamic> _cpContentMap(Map<String, dynamic> data) {
final content = data["content"];
try {

View File

@ -458,7 +458,7 @@ class _MessageItem extends StatelessWidget {
showMsgItemMenu(ct, "");
},
onTap: () {
_showCpInviteDialogFromMessage(data, cpInvite, actionState);
_openCpInviteDialogFromMessage(data, cpInvite, actionState);
},
child: Container(
width: 251.w,
@ -529,6 +529,30 @@ class _MessageItem extends StatelessWidget {
);
}
void _openCpInviteDialogFromMessage(
Map<String, dynamic> data,
SCSystemInvitMessageRes? cpInvite,
RoomCpInviteActionState actionState,
) {
unawaited(_openCpInviteDialogFromMessageAsync(data, cpInvite, actionState));
}
Future<void> _openCpInviteDialogFromMessageAsync(
Map<String, dynamic> data,
SCSystemInvitMessageRes? cpInvite,
RoomCpInviteActionState actionState,
) async {
final latestState = await _latestCpInviteStateForMessage(data);
if (!context.mounted) {
return;
}
final resolvedActionState =
latestState == _CpInviteMessageState.pending
? actionState
: _cpInviteActionState(latestState);
_showCpInviteDialogFromMessage(data, cpInvite, resolvedActionState);
}
Widget _buildCpInviteGiftThumb(String giftCover) {
final child =
giftCover.isNotEmpty
@ -788,6 +812,7 @@ class _MessageItem extends StatelessWidget {
applyId,
SCSysytemMessageType.CP_BUILD.name,
cpInvite: cpInvite,
cpData: data,
)
: null,
onReject:
@ -798,6 +823,7 @@ class _MessageItem extends StatelessWidget {
applyId,
SCSysytemMessageType.CP_BUILD.name,
cpInvite: cpInvite,
cpData: data,
)
: null,
);
@ -856,6 +882,56 @@ class _MessageItem extends StatelessWidget {
: _CpInviteMessageState.pending;
}
Future<_CpInviteMessageState> _latestCpInviteStateForMessage(
Map<String, dynamic> data,
) async {
final localState = _cpInviteStateForMessage(data);
if (localState != _CpInviteMessageState.pending) {
return localState;
}
final applyId = _cpApplyIdFromData(data);
if (applyId.isEmpty) {
return localState;
}
try {
final status = await SCAccountRepository().cpRelationshipApplyStatus(
applyId,
);
final backendState = _cpInviteStateFromBackendStatus(status);
if (backendState != null) {
return backendState;
}
} catch (_) {}
return _cpInviteSecondsLeft(data) <= 0
? _CpInviteMessageState.expired
: localState;
}
_CpInviteMessageState? _cpInviteStateFromBackendStatus(String? status) {
final text = status?.trim().toUpperCase() ?? "";
if (text.isEmpty || text == "WAIT" || text == "WAITING") {
return text.isEmpty ? null : _CpInviteMessageState.pending;
}
if (text.contains("EXPIRE") ||
text.contains("TIMEOUT") ||
text.contains("OVERDUE")) {
return _CpInviteMessageState.expired;
}
if (text.contains("REFUSE") ||
text.contains("REJECT") ||
text.contains("DECLIN") ||
text.contains("DENY")) {
return _CpInviteMessageState.rejected;
}
if (text.contains("AGREE") ||
text.contains("ACCEPT") ||
text.contains("APPROVE") ||
text.contains("ESTABLISH")) {
return _CpInviteMessageState.accepted;
}
return null;
}
_CpInviteMessageState _cpInviteStateFromConversation(
Map<String, dynamic> currentData,
) {
@ -1863,7 +1939,14 @@ class _MessageItem extends StatelessWidget {
Future<void> _acceptCpInviteDirect(
String id,
SCSystemInvitMessageRes? cpInvite,
Map<String, dynamic>? cpData,
) async {
if (!await _shouldProcessCpInvite(id, cpInvite, cpData)) {
return;
}
if (!context.mounted) {
return;
}
final profileManager = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
@ -1882,7 +1965,14 @@ class _MessageItem extends StatelessWidget {
Future<void> _rejectCpInviteDirect(
String id,
SCSystemInvitMessageRes? cpInvite,
Map<String, dynamic>? cpData,
) async {
if (!await _shouldProcessCpInvite(id, cpInvite, cpData)) {
return;
}
if (!context.mounted) {
return;
}
final profileManager = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
@ -1898,9 +1988,62 @@ class _MessageItem extends StatelessWidget {
}
}
void acceptOpt(String id, String type, {SCSystemInvitMessageRes? cpInvite}) {
Future<bool> _shouldProcessCpInvite(
String applyId,
SCSystemInvitMessageRes? cpInvite,
Map<String, dynamic>? data,
) async {
final latestState =
data == null
? await _latestCpInviteStateForApplyId(applyId)
: await _latestCpInviteStateForMessage(data);
if (latestState == _CpInviteMessageState.pending) {
return true;
}
if (!context.mounted) {
return false;
}
if (data != null) {
_showCpInviteDialogFromMessage(
data,
cpInvite,
_cpInviteActionState(latestState),
);
} else {
SmartDialog.dismiss(tag: RoomCpInviteDialog.dialogTag);
if (latestState == _CpInviteMessageState.expired) {
SCTts.show(SCAppLocalizations.of(context)!.closeFriendInviteTimedOut);
}
}
updateCall();
return false;
}
Future<_CpInviteMessageState> _latestCpInviteStateForApplyId(
String applyId,
) async {
if (applyId.trim().isEmpty) {
return _CpInviteMessageState.pending;
}
try {
final status = await SCAccountRepository().cpRelationshipApplyStatus(
applyId,
);
return _cpInviteStateFromBackendStatus(status) ??
_CpInviteMessageState.pending;
} catch (_) {
return _CpInviteMessageState.pending;
}
}
void acceptOpt(
String id,
String type, {
SCSystemInvitMessageRes? cpInvite,
Map<String, dynamic>? cpData,
}) {
if (type == SCSysytemMessageType.CP_BUILD.name) {
unawaited(_acceptCpInviteDirect(id, cpInvite));
unawaited(_acceptCpInviteDirect(id, cpInvite, cpData));
return;
}
SmartDialog.show(
@ -1948,9 +2091,14 @@ class _MessageItem extends StatelessWidget {
);
}
void rejectOpt(String id, String type, {SCSystemInvitMessageRes? cpInvite}) {
void rejectOpt(
String id,
String type, {
SCSystemInvitMessageRes? cpInvite,
Map<String, dynamic>? cpData,
}) {
if (type == SCSysytemMessageType.CP_BUILD.name) {
unawaited(_rejectCpInviteDirect(id, cpInvite));
unawaited(_rejectCpInviteDirect(id, cpInvite, cpData));
return;
}
SmartDialog.show(

View File

@ -434,8 +434,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
.addConversationListener(
listener: V2TimConversationListener(
onNewConversation: (conversationList) {
// _onRefreshConversation(conversationList);
initConversation();
_onRefreshConversation(conversationList);
},
onConversationChanged: (conversationList) {
_onRefreshConversation(conversationList);
},
onTotalUnreadMessageCountChanged: (int totalUnreadCount) {
messageUnReadCount = totalUnreadCount;
@ -605,7 +607,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
conversationMap.clear();
if (conversationList != null) {
for (V2TimConversation? conversation in conversationList) {
conversationMap[conversation!.conversationID] = conversation;
if (conversation == null) {
continue;
}
conversationMap[conversation.conversationID] = conversation;
_handleCpBuildFromConversation(conversation);
}
}
getConversationList();
@ -853,6 +859,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
if (data == null) {
return null;
}
final relationNoticePeerId = _cpRelationNoticePeerIdFromData(
data,
message: message,
);
if (relationNoticePeerId != null) {
return relationNoticePeerId;
}
final invite = _cpInviteFromMessageData(data);
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
if (invite == null || currentProfile == null) {
@ -930,6 +943,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
if (_handleCpInviteResultMessage(data, msgId: message?.msgID)) {
return;
}
if (_handleCpRelationRefreshNoticeMessage(data, message: message)) {
return;
}
if (noticeType != SCSysytemMessageType.CP_BUILD.name) {
return;
}
@ -1241,6 +1257,47 @@ class RealTimeMessagingManager extends ChangeNotifier {
return true;
}
bool _handleCpRelationRefreshNoticeMessage(
Map<String, dynamic> data, {
V2TimMessage? message,
}) {
final noticeType = _cpRelationNoticeTypeFromData(data);
if (!_isCpRelationRefreshNoticeType(noticeType)) {
return false;
}
_cpInviteLog(
'handle CP relation refresh notice noticeType=$noticeType raw=$data',
);
final context = navigatorKey.currentState?.context;
if (context != null) {
unawaited(
Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
).refreshLoadedCpProfiles(),
);
if (noticeType == SCSysytemMessageType.CP_DISMISS.name) {
final currentUserId =
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
final peerUserId = _cpRelationNoticePeerIdFromData(
data,
message: message,
);
if (currentUserId.isNotEmpty && peerUserId != null) {
Provider.of<RtcProvider?>(
context,
listen: false,
)?.invalidateRoomMicRelationBetween(
userId: currentUserId,
peerUserId: peerUserId,
relationType: _cpRelationTypeFromNoticeData(data),
);
}
}
}
return true;
}
Future<void> _showCpInviteResultDialog({
required BuildContext context,
required Map<String, dynamic> data,
@ -2039,13 +2096,88 @@ class RealTimeMessagingManager extends ChangeNotifier {
content["type"]?.toString(),
])?.toUpperCase() ??
"";
if (noticeType == SCSysytemMessageType.CP_BUILD.name) {
if (noticeType == SCSysytemMessageType.CP_BUILD.name ||
_isCpRelationRefreshNoticeType(noticeType)) {
return true;
}
return _cpInviteApplyIdFromData(data).isNotEmpty &&
_cpInviteResultFromMessageData(data) != null;
}
String _cpRelationNoticeTypeFromData(Map<String, dynamic> data) {
final content = _cpInviteContentMap(data);
return (_firstCpInviteValue([
data["noticeType"]?.toString(),
data["type"]?.toString(),
content["noticeType"]?.toString(),
content["type"]?.toString(),
]) ??
"")
.toUpperCase();
}
bool _isCpRelationRefreshNoticeType(String noticeType) {
return noticeType == SCSysytemMessageType.CP_DISMISS.name ||
noticeType == SCSysytemMessageType.CP_BLESS.name;
}
String _cpRelationTypeFromNoticeData(Map<String, dynamic> data) {
final content = _cpInviteContentMap(data);
return scNormalizeCpRelationType(
_firstCpInviteValue([
data["relationType"]?.toString(),
data["relation_type"]?.toString(),
content["relationType"]?.toString(),
content["relation_type"]?.toString(),
]) ??
"CP",
);
}
String? _cpRelationNoticePeerIdFromData(
Map<String, dynamic> data, {
V2TimMessage? message,
}) {
if (!_isCpRelationRefreshNoticeType(_cpRelationNoticeTypeFromData(data))) {
return null;
}
final content = _cpInviteContentMap(data);
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
final currentIds =
[currentProfile?.id, currentProfile?.account]
.map((value) => value?.trim() ?? "")
.where((value) => value.isNotEmpty)
.toSet();
final candidates = <String?>[
data["expand"]?.toString(),
content["expand"]?.toString(),
data["peerUserId"]?.toString(),
data["cpUserId"]?.toString(),
data["targetUserId"]?.toString(),
data["senderUserId"]?.toString(),
data["fromUserId"]?.toString(),
data["userId"]?.toString(),
content["peerUserId"]?.toString(),
content["cpUserId"]?.toString(),
content["targetUserId"]?.toString(),
content["senderUserId"]?.toString(),
content["fromUserId"]?.toString(),
content["userId"]?.toString(),
message?.sender,
message?.userID,
];
for (final candidate in candidates) {
final userId = candidate?.trim() ?? "";
if (userId.isEmpty ||
currentIds.contains(userId) ||
!scLooksLikeBackendUserId(userId)) {
continue;
}
return userId;
}
return null;
}
void _cpInviteLog(String message) {
debugPrint('[CP][Invite] $message');
}
@ -2053,10 +2185,23 @@ class RealTimeMessagingManager extends ChangeNotifier {
void _onRefreshConversation(List<V2TimConversation> conversations) {
for (V2TimConversation conversation in conversations) {
conversationMap[conversation.conversationID] = conversation;
_handleCpBuildFromConversation(conversation);
}
getConversationList();
}
void _handleCpBuildFromConversation(V2TimConversation conversation) {
final message = conversation.lastMessage;
if (message == null || message.groupID != null) {
return;
}
final data = _decodeCpSystemMessageData(message);
if (!_isCpRelationInviteNoticeData(data)) {
return;
}
unawaited(_handleIncomingCpBuildMessage(message));
}
///im群聊
Future<V2TimValueCallback<String>> createRoomGroup(
String groupID,

View File

@ -178,22 +178,28 @@ class SocialChatUserProfileManager extends ChangeNotifier {
if (userId.trim().isEmpty) {
return profile;
}
final cpPairs = <CPRes>[];
final closeFriendPairs = <CPRes>[];
final fallbackCpPairs = <CPRes>[];
final fallbackCloseFriendPairs = <CPRes>[];
_addCpRelationPairs(
cpPairs: cpPairs,
closeFriendPairs: closeFriendPairs,
cpPairs: fallbackCpPairs,
closeFriendPairs: fallbackCloseFriendPairs,
relations: profile.cpList,
fallbackRelationType: 'CP',
);
_addCpRelationPairs(
cpPairs: cpPairs,
closeFriendPairs: closeFriendPairs,
cpPairs: fallbackCpPairs,
closeFriendPairs: fallbackCloseFriendPairs,
relations: profile.closeFriendList,
);
final cpPairs = <CPRes>[];
final closeFriendPairs = <CPRes>[];
var loadedAnyCabin = false;
await Future.wait(
_cpProfileRelationTypes.map((relationType) async {
final cabin = await _safeLoadCpCabin(userId, relationType);
if (cabin != null) {
loadedAnyCabin = true;
}
final cpPair = cabin?.cpPairUserProfileCO;
if (cpPair == null || !_hasCpPair(cpPair)) {
return;
@ -231,6 +237,12 @@ class SocialChatUserProfileManager extends ChangeNotifier {
'closeFriends=${closeFriendPairs.length}',
);
}
if (!loadedAnyCabin) {
return profile.copyWith(
cpList: fallbackCpPairs,
closeFriendList: fallbackCloseFriendPairs,
);
}
return profile.copyWith(
cpList: cpPairs,
closeFriendList: closeFriendPairs,