diff --git a/lib/modules/chat/message_chat_page.dart b/lib/modules/chat/message_chat_page.dart index baa8c2d..be24c57 100644 --- a/lib/modules/chat/message_chat_page.dart +++ b/lib/modules/chat/message_chat_page.dart @@ -581,6 +581,14 @@ class _SCMessageChatPageState extends State { // 监听消息扩展更新 onRecvMessageExtensionsChanged: (msgID, extensions) { for (var ext in extensions) { + if (ext.extensionKey == scChatBubbleMessageExtensionKey) { + _MessageItem.updateChatBubbleExtensionCache( + msgID, + ext.extensionValue, + ); + setState(() {}); + continue; + } if (ext.extensionKey == "packetID") { String packetID = ext.extensionValue; SCMessageUtils.redPacketFutureCache[packetID] = @@ -985,6 +993,9 @@ class _SCMessageChatPageState extends State { class _MessageItem extends StatelessWidget { static bool _isOpeningCpInviteDialog = false; + static final Map> + _chatBubbleExtensionFutures = {}; + static final Map _chatBubbleExtensionCache = {}; final V2TimMessage message; final V2TimMessage? preMessage; @@ -2630,7 +2641,74 @@ class _MessageItem extends StatelessWidget { if (chatBubbleUrl.isNotEmpty) { return _buildVipTextBubble(content: content, imageUrl: chatBubbleUrl); } - return _buildDefaultTextBubble(content); + final msgID = message.msgID?.trim(); + if (msgID == null || + msgID.isEmpty || + message.elemType != MessageElemType.V2TIM_ELEM_TYPE_TEXT) { + return _buildDefaultTextBubble(content); + } + return FutureBuilder( + future: _chatBubbleFromMessageExtension(message), + builder: (_, snapshot) { + final extensionBubbleUrl = _resolveChatBubbleImageUrl(snapshot.data); + if (extensionBubbleUrl.isNotEmpty) { + return _buildVipTextBubble( + content: content, + imageUrl: extensionBubbleUrl, + ); + } + return _buildDefaultTextBubble(content); + }, + ); + } + + static void updateChatBubbleExtensionCache( + String msgID, + String extensionValue, + ) { + final id = msgID.trim(); + if (id.isEmpty) { + return; + } + _chatBubbleExtensionCache[id] = scDecodeChatBubbleSnapshotValue( + extensionValue, + ); + _chatBubbleExtensionFutures.remove(id); + } + + Future _chatBubbleFromMessageExtension( + V2TimMessage message, + ) { + final msgID = message.msgID?.trim(); + if (msgID == null || msgID.isEmpty) { + return Future.value(null); + } + if (_chatBubbleExtensionCache.containsKey(msgID)) { + return Future.value(_chatBubbleExtensionCache[msgID]); + } + return _chatBubbleExtensionFutures.putIfAbsent(msgID, () async { + if (message.isSupportMessageExtension == false) { + _chatBubbleExtensionCache[msgID] = null; + return null; + } + try { + final result = await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .getMessageExtensions(msgID: msgID); + final extensions = result.data ?? const []; + for (final extension in extensions) { + if (extension.extensionKey == scChatBubbleMessageExtensionKey) { + final bubble = scDecodeChatBubbleSnapshotValue( + extension.extensionValue, + ); + _chatBubbleExtensionCache[msgID] = bubble; + return bubble; + } + } + } catch (_) {} + _chatBubbleExtensionCache[msgID] = null; + return null; + }); } Widget _buildDefaultTextBubble(String content) { diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index 5bd7fe6..47cfbf4 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -776,14 +776,7 @@ class _VoiceRoomPageState extends State targetGroupKey, targetUserIds, ); - return; } - _handleStandardGiftComboVisuals( - msg, - giftPhoto, - targetGroupKey, - targetUserIds, - ); } void _enqueueCpGiftFloatingIfNeeded(Msg msg) { @@ -946,45 +939,6 @@ class _VoiceRoomPageState extends State } } - void _handleStandardGiftComboVisuals( - Msg msg, - String giftPhoto, - String? targetGroupKey, - List targetUserIds, - ) { - if ((msg.number ?? 0) <= 0) { - return; - } - if (!_shouldPlaySeatFlightGiftAnimation(msg) || - targetGroupKey == null || - targetUserIds.isEmpty || - giftPhoto.isEmpty) { - return; - } - final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds); - if (targetUserIds.length > 1 && - !_markGiftFlightBatchForPlayback(flightBatchKey)) { - return; - } - final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey); - final session = _luckyGiftComboSessions.putIfAbsent( - sessionKey, - () => _LuckyGiftComboSession(), - ); - session.endTimer?.cancel(); - session.clearQueueTimer?.cancel(); - - _enqueueTrackedSeatFlightAnimations( - sessionKey: sessionKey, - giftPhoto: giftPhoto, - targetUserIds: targetUserIds, - animationCount: _resolveTrackedAnimationCount(msg), - batchKey: flightBatchKey, - ); - - _scheduleGiftAnimationSessionEnd(sessionKey); - } - void _handleLuckyGiftComboVisuals( Msg msg, String giftPhoto, @@ -1357,26 +1311,6 @@ class _VoiceRoomPageState extends State _giftVisualBatchDedupeTimers.clear(); } - bool _shouldPlaySeatFlightGiftAnimation(Msg msg) { - final gift = msg.gift; - if (gift == null) { - return false; - } - - final giftPhoto = (gift.giftPhoto ?? "").trim(); - if (giftPhoto.isEmpty) { - return false; - } - final giftPhotoExt = _normalizedGiftResourceExtension(giftPhoto); - if (_isAnimatedGiftResource(giftPhotoExt)) { - return false; - } - - final giftSourceUrl = (gift.giftSourceUrl ?? "").trim(); - final sourceExt = _normalizedGiftResourceExtension(giftSourceUrl); - return !_isAnimatedGiftResource(sourceExt); - } - bool _isAnimatedGiftResource(String extension) { return extension == ".svga" || extension == ".mp4" || extension == ".vap"; } diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index b170606..90e919d 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -41,6 +41,7 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_result.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_image_elem.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_extension.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_msg_create_info_result.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; @@ -2644,14 +2645,18 @@ class RealTimeMessagingManager extends ChangeNotifier { if (createTextMessageRes.code == 0) { // 文本信息创建成功 String id = createTextMessageRes.data!.id!; + final chatBubble = + AccountStorage().getCurrentUser()?.userProfile?.getChatBox(); final chatBubbleCloudCustomData = scEncodeChatBubbleCloudCustomData( - AccountStorage().getCurrentUser()?.userProfile?.getChatBox(), + chatBubble, + ); + final chatBubbleSnapshotValue = scEncodeChatBubbleSnapshotValue( + chatBubble, ); // 发送文本消息 // 在sendMessage时,若只填写receiver则发个人用户单聊消息 // 若只填写groupID则发群组消息 // 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见 - String receiveId = toConversation.userID!; V2TimValueCallback sendMessageRes = await TencentImSDKPlugin .v2TIMManager .getMessageManager() @@ -2660,11 +2665,20 @@ class RealTimeMessagingManager extends ChangeNotifier { receiver: toConversation.userID!, // 接收人id needReadReceipt: true, groupID: '', // 是否需要已读回执 + isSupportMessageExtension: chatBubbleSnapshotValue != null, cloudCustomData: chatBubbleCloudCustomData, ); if (sendMessageRes.code == 0) { // 发送成功 _onNew1v1Message(sendMessageRes.data); + if (chatBubbleSnapshotValue != null) { + unawaited( + _setC2CChatBubbleMessageExtension( + sendMessageRes.data?.msgID, + chatBubbleSnapshotValue, + ), + ); + } } else { SCTts.show( 'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}', @@ -2673,6 +2687,33 @@ class RealTimeMessagingManager extends ChangeNotifier { } } + Future _setC2CChatBubbleMessageExtension( + String? msgID, + String snapshotValue, + ) async { + final id = msgID?.trim(); + if (id == null || id.isEmpty) { + return; + } + try { + await TencentImSDKPlugin.v2TIMManager + .getMessageManager() + .setMessageExtensions( + msgID: id, + extensions: [ + V2TimMessageExtension( + extensionKey: scChatBubbleMessageExtensionKey, + extensionValue: snapshotValue, + ), + ], + ); + } catch (error) { + if (kDebugMode) { + debugPrint('[chat_bubble] set extension failed msgID=$id error=$error'); + } + } + } + ///发送单聊自定义消息 Future sendC2CCustomMsg( String msg, diff --git a/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart index c6e9509..f3d15b0 100644 --- a/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart +++ b/lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart @@ -322,37 +322,82 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository { ///sys/version/manage/release/latest @override Future versionManageLatest() async { - final result = await http.get( - "ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687", - fromJson: (json) { - _debugVersionResponse('releaseLatest', json); - return SCVersionManageLatestRes.fromJson(json); - }, - ); - return result; + const apiName = 'releaseLatest'; + const path = '/sys/version/manage/release/latest'; + const queryParams = {}; + _debugVersionRequest(apiName, path, queryParams); + try { + final result = await http.get( + "ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687", + queryParams: queryParams, + fromJson: (json) { + _debugVersionResponse(apiName, path, json); + return SCVersionManageLatestRes.fromJson(json); + }, + ); + return result; + } catch (error) { + _debugVersionError(apiName, path, queryParams, error); + rethrow; + } } ///sys/version/manage/latest/review @override Future versionManageLatestReview() async { - final result = await http.get( - "ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232", - fromJson: (json) { - _debugVersionResponse('latestReview', json); - return VersionManageLatesReviewRes.fromJson(json); - }, - ); - return result; + const apiName = 'latestReview'; + const path = '/sys/version/manage/latest/review'; + const queryParams = {}; + _debugVersionRequest(apiName, path, queryParams); + try { + final result = await http.get( + "ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232", + queryParams: queryParams, + fromJson: (json) { + _debugVersionResponse(apiName, path, json); + return VersionManageLatesReviewRes.fromJson(json); + }, + ); + return result; + } catch (error) { + _debugVersionError(apiName, path, queryParams, error); + rethrow; + } } - void _debugVersionResponse(String name, dynamic json) { + void _debugVersionRequest( + String name, + String path, + Map queryParams, + ) { + debugPrint( + '[SCVersionUtils][API] $name request path=$path params=$queryParams', + ); + } + + void _debugVersionResponse(String name, String path, dynamic json) { try { - debugPrint('[SCVersionUtils] $name rawBody=${jsonEncode(json)}'); + debugPrint( + '[SCVersionUtils][API] $name response path=$path rawBody=${jsonEncode(json)}', + ); } catch (e) { - debugPrint('[SCVersionUtils] $name rawBody=$json encodeError=$e'); + debugPrint( + '[SCVersionUtils][API] $name response path=$path rawBody=$json encodeError=$e', + ); } } + void _debugVersionError( + String name, + String path, + Map queryParams, + Object error, + ) { + debugPrint( + '[SCVersionUtils][API] $name failed path=$path params=$queryParams error=$error', + ); + } + ///sys/config/customer-service @override Future customerService() async { diff --git a/lib/shared/tools/sc_chat_bubble_message_utils.dart b/lib/shared/tools/sc_chat_bubble_message_utils.dart index 6510d92..a105a13 100644 --- a/lib/shared/tools/sc_chat_bubble_message_utils.dart +++ b/lib/shared/tools/sc_chat_bubble_message_utils.dart @@ -3,14 +3,19 @@ import 'dart:convert'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; const String scChatBubbleCloudDataKey = 'scChatBubble'; +const String scChatBubbleMessageExtensionKey = scChatBubbleCloudDataKey; String? scEncodeChatBubbleCloudCustomData(PropsResources? chatBubble) { - if (!_hasChatBubbleImage(chatBubble)) { + final snapshot = scChatBubbleSnapshotJson(chatBubble); + if (snapshot == null) { return null; } - return jsonEncode({ - scChatBubbleCloudDataKey: chatBubble!.toJson(), - }); + return jsonEncode({scChatBubbleCloudDataKey: snapshot}); +} + +String? scEncodeChatBubbleSnapshotValue(PropsResources? chatBubble) { + final snapshot = scChatBubbleSnapshotJson(chatBubble); + return snapshot == null ? null : jsonEncode(snapshot); } PropsResources? scDecodeChatBubbleFromCloudCustomData(String? cloudCustomData) { @@ -31,6 +36,48 @@ PropsResources? scDecodeChatBubbleFromCloudCustomData(String? cloudCustomData) { return null; } +PropsResources? scDecodeChatBubbleSnapshotValue(String? snapshotValue) { + final raw = snapshotValue?.trim(); + if (raw == null || raw.isEmpty) { + return null; + } + try { + final decoded = jsonDecode(raw); + if (decoded is Map) { + return PropsResources.fromJson(decoded); + } + } catch (_) {} + return null; +} + +Map? scChatBubbleSnapshotJson(PropsResources? resource) { + if (!_hasChatBubbleImage(resource)) { + return null; + } + final map = {}; + void put(String key, String? value) { + final text = value?.trim(); + if (text != null && text.isNotEmpty) { + map[key] = text; + } + } + + put('id', resource?.id); + put('name', resource?.name); + put('type', resource?.type); + put('cover', resource?.cover); + put('expand', resource?.expand); + put('sourceUrl', resource?.sourceUrl); + put('imSendCoverUrl', resource?.imSendCoverUrl); + put('imOpenedUrl', resource?.imOpenedUrl); + put('imOpenedUrlTwo', resource?.imOpenedUrlTwo); + put('imNotOpenedUrl', resource?.imNotOpenedUrl); + put('roomSendCoverUrl', resource?.roomSendCoverUrl); + put('roomOpenedUrl', resource?.roomOpenedUrl); + put('roomNotOpenedUrl', resource?.roomNotOpenedUrl); + return map; +} + bool _hasChatBubbleImage(PropsResources? resource) { if (resource == null) { return false; diff --git a/lib/shared/tools/sc_version_utils.dart b/lib/shared/tools/sc_version_utils.dart index 8739d6d..0424ee2 100644 --- a/lib/shared/tools/sc_version_utils.dart +++ b/lib/shared/tools/sc_version_utils.dart @@ -29,10 +29,17 @@ class SCVersionUtils { 'review=${_versionInfoDebug(versionInfo.review)} ' 'latest=${_versionInfoDebug(versionInfo.latest)}', ); + final review = _selectCurrentPlatformVersion( + versionInfo.review, + source: 'latestReview.review', + ); if (versionInfo.review != null) { SCGlobalConfig.isReview = - "${versionInfo.review?.version}" == SCGlobalConfig.version; - _debugLog('checkReview isReview=${SCGlobalConfig.isReview}'); + review != null && "${review.version}" == SCGlobalConfig.version; + _debugLog( + 'checkReview isReview=${SCGlobalConfig.isReview} ' + 'selectedReview=${_versionInfoDebug(review)}', + ); } final latest = await _resolveLatestVersion(versionInfo.latest); final shouldForceUpdate = latest != null && _shouldForceUpdate(latest); @@ -96,20 +103,56 @@ class SCVersionUtils { static Future _resolveLatestVersion( SCVersionManageLatestRes? latest, ) async { - if (!_isVersionInfoEmpty(latest)) { - return latest; + final selectedLatest = _selectCurrentPlatformVersion( + latest, + source: 'latestReview.latest', + ); + if (selectedLatest != null) { + return selectedLatest; } - _debugLog('checkReview latestReview latest empty, fallback releaseLatest'); + _debugLog( + 'checkReview latestReview latest unavailable for current platform, ' + 'fallback releaseLatest', + ); try { final fallback = await SCConfigRepositoryImp().versionManageLatest(); _debugLog('checkReview fallbackLatest=${_versionInfoDebug(fallback)}'); - return _isVersionInfoEmpty(fallback) ? latest : fallback; + return _selectCurrentPlatformVersion(fallback, source: 'releaseLatest'); } catch (e) { _debugLog('checkReview fallback releaseLatest failed error=$e'); - return latest; + return null; } } + static SCVersionManageLatestRes? _selectCurrentPlatformVersion( + SCVersionManageLatestRes? version, { + required String source, + }) { + if (_isVersionInfoEmpty(version)) { + _debugLog('checkReview ignore $source reason=empty'); + return null; + } + final currentVersion = version!; + if (_isCurrentPlatformVersion(currentVersion)) { + _debugLog( + 'checkReview select $source currentPlatform=${_currentPlatformName()} ' + 'version=${currentVersion.version} ' + 'buildVersion=${currentVersion.buildVersion} ' + 'downloadUrl=${currentVersion.downloadUrl}', + ); + return currentVersion; + } + _debugLog( + 'checkReview ignore $source reason=platform_mismatch ' + 'currentPlatform=${_currentPlatformName()} ' + 'responsePlatform=${currentVersion.platform} ' + 'version=${currentVersion.version} ' + 'buildVersion=${currentVersion.buildVersion} ' + 'downloadUrl=${currentVersion.downloadUrl}', + ); + return null; + } + static bool _isVersionInfoEmpty(SCVersionManageLatestRes? version) { if (version == null) { return true; @@ -145,6 +188,25 @@ class SCVersionUtils { return 0; } + static bool _isCurrentPlatformVersion(SCVersionManageLatestRes version) { + return _normalizePlatform(version.platform) == _currentPlatformKey(); + } + + static String _currentPlatformName() { + return Platform.isIOS ? 'iOS' : 'Android'; + } + + static String _currentPlatformKey() { + return Platform.isIOS ? 'ios' : 'android'; + } + + static String _normalizePlatform(String? platform) { + return (platform ?? '').trim().toLowerCase().replaceAll( + RegExp(r'[^a-z0-9]'), + '', + ); + } + static List _versionParts(String? version) { final normalized = version?.trim() ?? ""; if (normalized.isEmpty) { @@ -277,6 +339,14 @@ class SCVersionUtils { static Future _openUpdateUrl(SCVersionManageLatestRes latest) async { final url = _resolveDownloadUrl(latest); + final urlSource = + (latest.downloadUrl ?? "").trim().isNotEmpty + ? 'api_downloadUrl' + : (Platform.isIOS ? 'local_appStore' : 'local_googlePlay'); + _debugLog( + 'openUpdateUrl currentPlatform=${_currentPlatformName()} ' + 'responsePlatform=${latest.platform} source=$urlSource url=$url', + ); if (url.isEmpty) { return; }