This commit is contained in:
roxy 2026-05-09 18:15:32 +08:00
parent c810e645dc
commit 53dbacef91
12 changed files with 484 additions and 24 deletions

View File

@ -6,9 +6,13 @@
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
@ -130,6 +134,12 @@
android:name="io.agora.rtc2.extensions.MediaProjectionMgr$LocalScreenSharingService"
tools:node="remove" />
<service
android:name=".VoiceRoomForegroundService"
android:exported="false"
android:foregroundServiceType="mediaPlayback|microphone"
android:stopWithTask="false" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="high_importance_channel" />

View File

@ -30,6 +30,33 @@ class MainActivity : FlutterActivity() {
else -> result.notImplemented()
}
}
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
VOICE_ROOM_FOREGROUND_CHANNEL
).setMethodCallHandler { call, result ->
when (call.method) {
"start" -> {
try {
VoiceRoomForegroundService.start(
applicationContext,
call.argument<String>("roomName")
)
result.success(null)
} catch (e: Exception) {
result.error("VOICE_ROOM_FOREGROUND_START_FAILED", e.message, null)
}
}
"stop" -> {
try {
VoiceRoomForegroundService.stop(applicationContext)
result.success(null)
} catch (e: Exception) {
result.error("VOICE_ROOM_FOREGROUND_STOP_FAILED", e.message, null)
}
}
else -> result.notImplemented()
}
}
}
override fun onDestroy() {
@ -90,5 +117,7 @@ class MainActivity : FlutterActivity() {
companion object {
private const val INSTALL_REFERRER_CHANNEL = "com.org.yumiparty/install_referrer"
private const val MOBILE_CONTEXT_CHANNEL = "com.org.yumiparty/mobile_context"
private const val VOICE_ROOM_FOREGROUND_CHANNEL =
"com.org.yumiparty/voice_room_foreground_service"
}
}

View File

@ -0,0 +1,131 @@
package com.org.yumiparty
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
class VoiceRoomForegroundService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
ensureNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val roomName = intent?.getStringExtra(EXTRA_ROOM_NAME)
?.trim()
?.takeIf { it.isNotEmpty() }
?: DEFAULT_ROOM_NAME
startAsForeground(roomName)
return START_NOT_STICKY
}
private fun startAsForeground(roomName: String) {
val notification = buildNotification(roomName)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val serviceType =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
} else {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
}
startForeground(
NOTIFICATION_ID,
notification,
serviceType
)
} else {
startForeground(NOTIFICATION_ID, notification)
}
}
@Suppress("DEPRECATION")
private fun buildNotification(roomName: String): Notification {
val launchIntent = (
packageManager.getLaunchIntentForPackage(packageName)
?: Intent(this, MainActivity::class.java)
).apply {
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val pendingIntent = PendingIntent.getActivity(
this,
0,
launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or immutableFlag()
)
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, CHANNEL_ID)
} else {
Notification.Builder(this)
}
return builder
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("Yumi voice room")
.setContentText(roomName)
.setOngoing(true)
.setShowWhen(false)
.setOnlyAlertOnce(true)
.setPriority(Notification.PRIORITY_LOW)
.setCategory(Notification.CATEGORY_CALL)
.setContentIntent(pendingIntent)
.build()
}
private fun ensureNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
val manager = getSystemService(NotificationManager::class.java)
if (manager.getNotificationChannel(CHANNEL_ID) != null) {
return
}
val channel = NotificationChannel(
CHANNEL_ID,
"Voice room",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Keeps voice rooms active while Yumi is in the background."
setShowBadge(false)
}
manager.createNotificationChannel(channel)
}
private fun immutableFlag(): Int =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
companion object {
private const val CHANNEL_ID = "yumi_voice_room"
private const val NOTIFICATION_ID = 24031
private const val EXTRA_ROOM_NAME = "room_name"
private const val DEFAULT_ROOM_NAME = "Voice room is running"
fun start(context: Context, roomName: String?) {
val intent = Intent(context, VoiceRoomForegroundService::class.java)
.putExtra(EXTRA_ROOM_NAME, roomName)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
fun stop(context: Context) {
context.stopService(Intent(context, VoiceRoomForegroundService::class.java))
}
}
}

View File

@ -502,7 +502,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10;
CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = S9X2AJ2US9;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -693,7 +693,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10;
CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -722,7 +722,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10;
CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@ -15,6 +15,7 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
import 'package:yumi/services/room/rc_room_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/services/audio/voice_room_foreground_service.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
@ -302,6 +303,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
'roomStartupSeatInteractionLoading';
static const String _selfMicGoUpRetryLoadingTag = 'selfMicGoUpRetryLoading';
static const int _selfMicGoUpFailureLimit = 3;
static const List<Duration> _roomRedPacketEntryRefreshDelays = [
Duration.zero,
Duration(seconds: 2),
Duration(seconds: 6),
];
bool needUpDataUserInfo = false;
bool _roomVisualEffectsEnabled = false;
@ -443,6 +449,21 @@ class RealTimeCommunicationManager extends ChangeNotifier {
bool get shouldShowRoomVisualEffects =>
currenRoom != null && _roomVisualEffectsEnabled;
void _startVoiceRoomForegroundService() {
final roomProfile = currenRoom?.roomProfile?.roomProfile;
final roomName = (roomProfile?.roomName ?? "").trim();
final roomAccount = (roomProfile?.roomAccount ?? "").trim();
unawaited(
SCVoiceRoomForegroundService.start(
roomName: roomName.isNotEmpty ? roomName : roomAccount,
),
);
}
void _stopVoiceRoomForegroundService() {
unawaited(SCVoiceRoomForegroundService.stop());
}
bool get _shouldDeferSeatInteractionForRoomStartup =>
_roomStartupStatus == RoomStartupStatus.loading;
@ -1943,6 +1964,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Future<void> releaseRtcEngineForAppTermination() async {
final rtcEngine = engine;
if (rtcEngine == null) {
await SCVoiceRoomForegroundService.stop();
return;
}
@ -1968,6 +1990,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} catch (e) {
debugPrint('rtc释放引擎出错: $e');
}
await SCVoiceRoomForegroundService.stop();
}
void initializeAudioVolumeIndicationCallback(
@ -2114,6 +2137,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (!context.mounted) {
return;
}
_startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context);
return;
}
@ -2137,6 +2161,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_startRoomStatePolling();
_setRoomStartupReady();
setRoomVisualEffectsEnabled(true);
_startRoomRedPacketPresenceHeartbeat(roomId);
_refreshRoomRedPacketListAfterEntry(roomId: roomId);
_startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context);
} else {
if (currenRoom != null) {
@ -2157,6 +2184,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
previewData: previewData,
previewSeatCount: previewSeatCount,
);
_startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context);
notifyListeners();
final entryRequestSerial = ++_roomEntryRequestSerial;
@ -2192,6 +2220,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom);
_syncCurrentUserVipHintFromRoom(currenRoom);
_startVoiceRoomForegroundService();
unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial));
_currentRoomIsEntryPreview = false;
_previewRoomSeatCount = null;
@ -2207,6 +2236,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return;
}
debugPrint('[RoomStartup] entry room failed: $e\n$stackTrace');
_stopVoiceRoomForegroundService();
_setRoomStartupFailed(RoomStartupFailureType.entry);
}
}
@ -2674,6 +2704,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
debugPrint(
'[RoomStartup] join IM group failed code=${joinResult?.code} desc=${joinResult?.desc}',
);
_stopVoiceRoomForegroundService();
_setRoomStartupFailed(RoomStartupFailureType.im);
return;
}
@ -2693,6 +2724,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
rtmProvider?.loadRoomHistoryMessages(groupId, count: 20) ??
Future<void>.value(),
);
_refreshRoomRedPacketListAfterEntry(roomId: roomId);
await Future.wait([microphoneListFuture, onlineUsersFuture]);
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
@ -2748,6 +2780,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return;
}
debugPrint('[RoomStartup] bootstrap failed: $error\n$stackTrace');
_stopVoiceRoomForegroundService();
_setRoomStartupFailed(failureType);
}
}
@ -2988,11 +3021,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Future<List<SCRoomRedPacketListRes>> loadRoomRedPacketList(
int current,
) async {
final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
if (roomId.isEmpty) {
return const <SCRoomRedPacketListRes>[];
}
var result = await SCChatRoomRepository().roomRedPacketList(
currenRoom?.roomProfile?.roomProfile?.id ?? "",
roomId,
current,
);
if (current == 1) {
final currentRoomId =
currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
if (current == 1 && currentRoomId == roomId) {
redPacketList = result;
notifyListeners();
}
@ -3055,19 +3094,18 @@ class RealTimeCommunicationManager extends ChangeNotifier {
);
}
void _refreshRoomRedPacketListAfterEntry() {
final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
if (roomId.isEmpty) {
void _refreshRoomRedPacketListAfterEntry({String? roomId}) {
final normalizedRoomId =
roomId?.trim() ??
currenRoom?.roomProfile?.roomProfile?.id?.trim() ??
"";
if (normalizedRoomId.isEmpty) {
return;
}
for (final delay in const [
Duration.zero,
Duration(seconds: 2),
Duration(seconds: 6),
]) {
for (final delay in _roomRedPacketEntryRefreshDelays) {
unawaited(
Future<void>.delayed(delay).then((_) {
return _refreshRoomRedPacketListIfCurrent(roomId);
return _refreshRoomRedPacketListIfCurrent(normalizedRoomId);
}),
);
}
@ -3381,6 +3419,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
///
void _clearData() {
_roomEntryRequestSerial += 1;
_stopVoiceRoomForegroundService();
_roomEntryEffectTimer?.cancel();
_roomEntryEffectTimer = null;
_stopRoomRedPacketPresenceHeartbeat();

View File

@ -571,6 +571,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
logined = true;
print('tim 登录成功');
await initConversation();
unawaited(syncRoomRedPacketBroadcastGroup());
} else {
//
//print('timm 需要重新登录2');
@ -591,16 +592,26 @@ class RealTimeMessagingManager extends ChangeNotifier {
_onNewMessage(V2TimMessage message) {
if (message.groupID != null) {
final groupId = message.groupID!;
/// / 广
if (message.groupID == SCGlobalConfig.bigBroadcastGroup ||
message.groupID == roomRedPacketBroadcastGroupId) {
_newBroadCastMsgRecv(message.groupID!, message);
if (groupId == SCGlobalConfig.bigBroadcastGroup ||
groupId == roomRedPacketBroadcastGroupId) {
_newBroadCastMsgRecv(groupId, message);
return;
}
if (_shouldRouteUntrackedRoomRedPacketBroadcast(groupId, message)) {
debugPrint(
'[RoomRedPacket][IM] route untracked group as region broadcast '
'group=$groupId cachedRegionGroup=$roomRedPacketBroadcastGroupId',
);
_newBroadCastMsgRecv(groupId, message, forceRegionBroadcast: true);
return;
}
///
for (var element in onNewMessageListenerGroupMap.values) {
element?.call(message.groupID!, message);
element?.call(groupId, message);
}
} else {
///
@ -1780,6 +1791,38 @@ class RealTimeMessagingManager extends ChangeNotifier {
'';
}
String _currentVoiceRoomGroupId() {
final currentContext = context;
if (currentContext == null || !currentContext.mounted) {
return '';
}
return Provider.of<RealTimeCommunicationManager>(
currentContext,
listen: false,
).currenRoom?.roomProfile?.roomProfile?.roomAccount?.trim() ??
'';
}
bool _shouldRouteUntrackedRoomRedPacketBroadcast(
String groupId,
V2TimMessage message,
) {
if (groupId == _currentVoiceRoomGroupId()) {
return false;
}
final customData = message.customElem?.data;
if (customData == null || customData.isEmpty) {
return false;
}
try {
final decoded = json.decode(customData);
final payload = _broadcastPayloadMap(decoded);
return payload['type'] == SCRoomMsgType.roomRedPacket;
} catch (_) {
return false;
}
}
Map<String, dynamic> _broadcastPayloadMap(dynamic data) {
if (data is Map<String, dynamic>) {
return data;
@ -1902,11 +1945,16 @@ class RealTimeMessagingManager extends ChangeNotifier {
}
///广
_newBroadCastMsgRecv(String groupID, V2TimMessage message) async {
_newBroadCastMsgRecv(
String groupID,
V2TimMessage message, {
bool forceRegionBroadcast = false,
}) async {
try {
String? customData = message.customElem?.data;
if (customData != null && customData.isNotEmpty) {
final isRegionBroadcastGroup = groupID == roomRedPacketBroadcastGroupId;
final isRegionBroadcastGroup =
forceRegionBroadcast || groupID == roomRedPacketBroadcastGroupId;
final data = json.decode(customData);
var type = data["type"];
if (type == "SYS_ACTIVITY") {
@ -1988,7 +2036,25 @@ class RealTimeMessagingManager extends ChangeNotifier {
} else if (type == SCRoomMsgType.roomRedPacket) {
///
var fData = data["data"];
final payload = _broadcastPayloadMap(fData);
final packetId = _payloadText(payload['packetId']);
final packetRoomId = _payloadText(payload["roomId"]);
debugPrint(
'[RoomRedPacket][IM] recv broadcast packetId=$packetId '
'roomId=$packetRoomId group=$groupID '
'regionGroup=$roomRedPacketBroadcastGroupId '
'isRegion=$isRegionBroadcastGroup '
'payloadRegion=${_payloadText(payload['regionCode'])} '
'currentRegion=${roomRedPacketBroadcastRegionCode ?? ''} '
'currentRoom=${_currentVoiceRoomId()}',
);
if (!_isSameRoomRedPacketRegion(fData)) {
debugPrint(
'[RoomRedPacket][IM] skip broadcast region mismatch '
'packetId=$packetId '
'payloadRegion=${_payloadText(payload['regionCode'])} '
'currentRegion=${roomRedPacketBroadcastRegionCode ?? ''}',
);
return;
}
RoomRedPacketPendingCache.instance.upsertPayload(fData);
@ -2007,7 +2073,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
: '',
priority: 1000,
);
final packetRoomId = _payloadText(fData["roomId"]);
if (_isCurrentVisibleVoiceRoom(packetRoomId)) {
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
context!,
@ -2616,6 +2681,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
return;
}
debugPrint(
'[RoomRedPacket][IM] send broadcast packetId=${payload['packetId']} '
'roomId=${payload['roomId']} mode=${payload['packetMode']} '
'targets=${targetGroupIds.join(',')} regionGroup=$regionGroupId '
'region=${payload['regionCode'] ?? roomRedPacketBroadcastRegionCode ?? ''}',
);
final message = <String, dynamic>{
'type': SCRoomMsgType.roomRedPacket,
'data': payload,

View File

@ -0,0 +1,38 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class SCVoiceRoomForegroundService {
static const MethodChannel _channel = MethodChannel(
'com.org.yumiparty/voice_room_foreground_service',
);
static Future<void> start({String? roomName}) async {
if (!Platform.isAndroid) {
return;
}
try {
await _channel.invokeMethod<void>('start', {
'roomName': roomName?.trim(),
});
} on PlatformException catch (e) {
debugPrint('[VoiceRoomForeground] start failed: ${e.message}');
} catch (e) {
debugPrint('[VoiceRoomForeground] start failed: $e');
}
}
static Future<void> stop() async {
if (!Platform.isAndroid) {
return;
}
try {
await _channel.invokeMethod<void>('stop');
} on PlatformException catch (e) {
debugPrint('[VoiceRoomForeground] stop failed: ${e.message}');
} catch (e) {
debugPrint('[VoiceRoomForeground] stop failed: $e');
}
}
}

View File

@ -24,12 +24,14 @@ typedef FloatingScreenManager = OverlayManager;
class OverlayManager {
static const int _maxLowPerformanceCompensationQueueLength = 8;
static const int _maxDeferredRegionRedPacketCount = 12;
static const Duration _compensationDedupWindow = Duration(seconds: 3);
static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30);
final SCPriorityQueue<SCFloatingMessage> _messageQueue = SCPriorityQueue(
(a, b) => b.priority.compareTo(a.priority),
);
final List<SCFloatingMessage> _deferredRegionRedPacketMessages = [];
final Map<String, DateTime> _recentCompensationKeys = {};
final Map<String, DateTime> _recentFloatingKeys = {};
bool _isPlaying = false;
@ -53,6 +55,12 @@ class OverlayManager {
if (_isRecentFloatingDuplicate(message)) {
return;
}
if (message.type == 4) {
debugPrint(
'[RoomRedPacket][Floating] enqueue packetId=${message.toUserId} '
'roomId=${message.roomId} region=${message.isRegionBroadcast}',
);
}
_rememberFloatingMessage(message);
_enqueueMessage(
message,
@ -61,6 +69,7 @@ class OverlayManager {
} else {
_removeActiveMessage(scheduleNext: false);
_messageQueue.clear();
_deferredRegionRedPacketMessages.clear();
_isPlaying = false;
_isProcessing = false;
_isDisposed = false;
@ -256,9 +265,20 @@ class OverlayManager {
//
if (_messageQueue.isEmpty) return;
final messageToProcess = _messageQueue.first;
if (messageToProcess == null) {
_messageQueue.removeFirst();
_safeScheduleNext();
return;
}
if (!_shouldDisplayMessage(context, messageToProcess)) {
_messageQueue.removeFirst();
if (_shouldDeferRegionRedPacketMessage(context, messageToProcess)) {
_deferRegionRedPacketMessage(context, messageToProcess);
_safeScheduleNext();
return;
}
_logRegionRedPacketDrop(context, messageToProcess);
_safeScheduleNext();
return;
}
@ -401,6 +421,7 @@ class OverlayManager {
_removeRegionBroadcastMessages();
return;
}
_restoreDeferredRegionRedPacketMessages();
_safeScheduleNext();
}
@ -434,8 +455,10 @@ class OverlayManager {
if (SCGlobalConfig.isFloatingBroadcastClosed) {
_removeActiveMessage(scheduleNext: false);
_messageQueue.clear();
_deferredRegionRedPacketMessages.clear();
return;
}
_restoreDeferredRegionRedPacketMessages();
final context = navigatorKey.currentState?.context;
final activeMessage = _currentMessage;
if (context != null &&
@ -456,6 +479,7 @@ class OverlayManager {
_currentOverlayEntry = null;
_currentMessage = null;
_messageQueue.clear();
_deferredRegionRedPacketMessages.clear();
_recentCompensationKeys.clear();
_isPlaying = false;
_isProcessing = false;
@ -497,6 +521,86 @@ class OverlayManager {
bool get _isSuppressed => _suppressedCount > 0;
bool _isRegionRedPacketMessage(SCFloatingMessage message) {
return message.type == 4 && message.isRegionBroadcast;
}
bool _shouldDeferRegionRedPacketMessage(
BuildContext context,
SCFloatingMessage message,
) {
if (!_isRegionRedPacketMessage(message) ||
SCGlobalConfig.isFloatingBroadcastClosed) {
return false;
}
return !_shouldDisplayRegionBroadcastMessage(context, message);
}
void _deferRegionRedPacketMessage(
BuildContext context,
SCFloatingMessage message,
) {
final key = _floatingDedupKeyFor(message);
if (key != null &&
_deferredRegionRedPacketMessages.any(
(item) => _floatingDedupKeyFor(item) == key,
)) {
return;
}
while (_deferredRegionRedPacketMessages.length >=
_maxDeferredRegionRedPacketCount) {
_deferredRegionRedPacketMessages.removeAt(0);
}
_deferredRegionRedPacketMessages.add(message);
debugPrint(
'[RoomRedPacket][Floating] defer packetId=${message.toUserId} '
'roomId=${message.roomId} ${_displayStateForLog(context)}',
);
}
void _restoreDeferredRegionRedPacketMessages() {
if (_deferredRegionRedPacketMessages.isEmpty) {
return;
}
final messages = List<SCFloatingMessage>.from(
_deferredRegionRedPacketMessages,
);
_deferredRegionRedPacketMessages.clear();
for (final message in messages) {
_messageQueue.add(message);
}
debugPrint(
'[RoomRedPacket][Floating] restore deferred count=${messages.length}',
);
}
void _logRegionRedPacketDrop(
BuildContext context,
SCFloatingMessage message,
) {
if (message.type != 4) {
return;
}
debugPrint(
'[RoomRedPacket][Floating] drop packetId=${message.toUserId} '
'roomId=${message.roomId} region=${message.isRegionBroadcast} '
'${_displayStateForLog(context)}',
);
}
String _displayStateForLog(BuildContext context) {
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
context,
listen: false,
);
final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
return 'homeVisible=$_homeRootTabsVisible '
'roomVisible=${rtcProvider.shouldShowRoomVisualEffects} '
'currentRoomId=$currentRoomId '
'scope=${SCGlobalConfig.floatingBroadcastScope}';
}
bool _shouldDisplayMessage(BuildContext context, SCFloatingMessage? message) {
if (message == null) {
return false;
@ -582,7 +686,7 @@ class OverlayManager {
while (_messageQueue.isNotEmpty) {
final message = _messageQueue.removeFirst();
if (!message.isRegionBroadcast) {
if (!message.isRegionBroadcast || _isRegionRedPacketMessage(message)) {
newQueue.add(message);
}
}

View File

@ -607,6 +607,11 @@ class _RoomRedPacketSendPanelState extends State<RoomRedPacketSendPanel> {
(_selectedPickupMinutes > 0 ? _selectedPickupMinutes * 60 : 0)
..['claimStartTime'] = claimStartTime
..['grabbed'] = result.grabbed ?? false;
final roomRegionCode = rtcProvider.currenRoom?.roomProfile?.regionCode;
if ((payload['regionCode']?.toString().trim() ?? '').isEmpty &&
(roomRegionCode?.trim().isNotEmpty ?? false)) {
payload['regionCode'] = roomRegionCode!.trim();
}
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
rtmProvider.addMsg(

View File

@ -68,6 +68,7 @@ class _RoomRedPacketFloatingEntryState
Timer? _entryRefreshTimer;
bool _refreshingList = false;
int _elapsedSeconds = 0;
String _observedRoomId = '';
String _verifiedRoomId = '';
int _verifiedRoomRefreshCount = 0;
Set<String> _verifiedKnownPacketIds = const <String>{};
@ -80,6 +81,11 @@ class _RoomRedPacketFloatingEntryState
const Duration(seconds: 1),
(_) => _onEntryTimerTick(),
);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_refreshRoomRedPacketList();
}
});
}
@override
@ -93,6 +99,7 @@ class _RoomRedPacketFloatingEntryState
if (!mounted) {
return;
}
_refreshRoomRedPacketListIfRoomChanged();
setState(() {});
_elapsedSeconds++;
if (_elapsedSeconds % _listRefreshIntervalSeconds == 0) {
@ -100,6 +107,24 @@ class _RoomRedPacketFloatingEntryState
}
}
void _refreshRoomRedPacketListIfRoomChanged() {
try {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
if (currentRoomId.isEmpty || currentRoomId == _observedRoomId) {
return;
}
_observedRoomId = currentRoomId;
_elapsedSeconds = 0;
_verifiedRoomId = '';
_verifiedRoomRefreshCount = 0;
_verifiedKnownPacketIds = const <String>{};
_verifiedVisiblePacketIds = const <String>{};
_refreshRoomRedPacketList();
} catch (_) {}
}
void _refreshRoomRedPacketList() {
if (_refreshingList || !mounted) {
return;
@ -116,6 +141,13 @@ class _RoomRedPacketFloatingEntryState
rtcProvider
.loadRoomRedPacketList(1)
.then((result) {
final activeRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id
?.trim() ??
'';
if (activeRoomId != currentRoomId) {
return result;
}
final serverPackets =
result.map(RoomRedPacketUiData.fromListRes).toList();
final serverPacketIds =

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.3.0+10
version: 1.3.0+11
environment:

View File

@ -562,6 +562,7 @@
- 已修正红包领取成功公屏消息的多语言问题:领取后不再发送已翻译好的普通文本,改为 `ROOM_RED_PACKET_CLAIM` 结构化房间 IM只传领取金额、红包发送人名和 `packetId`,公屏渲染时按当前客户端语言生成文案,避免英文用户看到阿语消息。
- 已补强房外延时红包飘屏和领取通知去重:首页四个一级 Tab 可见/回到前台时会重新同步当前语区红包 IM 群;语区红包飘屏在首页 Tab 中不再受“只看房间飘屏”设置影响;领取成功公屏通知按 `packetId + 领取人ID` 生成稳定去重键,避免本地插入和 IM 回包/历史拉取造成两条一模一样的领取消息。
- 已定位并修复公屏上滑历史反复弹 `user info not found`:历史消息 item 重建时会重复调用用户资料接口,缺失用户返回错误后被网络层 toast现在房间公屏补拉用户资料改为静默请求并对同一 userId 的请求/失败结果做缓存5 分钟内不再反复请求同一个不存在用户。
- 已继续排查房外/跨房红包飘屏缺失IM 重登成功后会立即重新同步当前语区红包群;收到未登记但内容为 `ROOM_RED_PACKET` 的群消息时会按语区红包飘屏兜底处理,避免语区群 id 尚未写入时被普通群消息分支吞掉;区域红包在首页 Tab 暂不可见时不再直接从飘屏队列丢弃,而是挂起后在回到首页 Tab 时恢复播放;发送红包 payload 也会补齐当前房间 `regionCode`,便于日志核对和前端兜底过滤。
## 已知问题
- 已接入语音房表情包素材第一版:桌面 `表情素材` 中的 `fluent_emoji``monkey` gif 已纳入项目资源,底部聊天入口改为图稿里的短白色胶囊并移除内部输入图标,右侧新增独立表情按钮;同一个房间输入弹层现在可从表情按钮直接打开表情包面板,原 unicode emoji 面板已替换为本地 gif 表情。发送逻辑同步按最新规则分流:用户在麦上发表情时只在自己麦位播放,不进入聊天列表;用户未上麦发表情时进入房间聊天框展示,不触发麦位动画。