From 39464469bae2c1a116b700c64fb87f7664017ed7 Mon Sep 17 00:00:00 2001 From: roxy Date: Mon, 27 Apr 2026 11:17:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E9=BA=A6/=E8=A1=A8=E6=83=85?= =?UTF-8?q?=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/gift/gift_page.dart | 7 +- lib/modules/gift/gift_tab_page.dart | 150 ++++++------------ lib/modules/room/seat/sc_seat_item.dart | 40 ++++- lib/modules/room/voice_room_page.dart | 6 +- lib/modules/user/me_page2.dart | 6 - lib/services/audio/rtc_manager.dart | 64 ++++++-- .../widgets/room/seat/room_seat_widget.dart | 28 ++++ test/room_seat_widget_test.dart | 32 ++++ 8 files changed, 197 insertions(+), 136 deletions(-) diff --git a/lib/modules/gift/gift_page.dart b/lib/modules/gift/gift_page.dart index e5e420b..3b9d27c 100644 --- a/lib/modules/gift/gift_page.dart +++ b/lib/modules/gift/gift_page.dart @@ -68,7 +68,6 @@ class _GiftPageState extends State with TickerProviderStateMixin { "LUCKY_GIFT", "CP", "MAGIC", - "CUSTOMIZED", "NSCIONAL_FLAG", ]; @@ -212,7 +211,10 @@ class _GiftPageState extends State with TickerProviderStateMixin { void initState() { super.initState(); rtcProvider = Provider.of(context, listen: false); - Provider.of(context, listen: false).giftList(); + Provider.of( + context, + listen: false, + ).giftList(includeCustomized: false); Provider.of(context, listen: false).giftActivityList(); // Provider.of(context, listen: false).giftBackpack(); Provider.of(context, listen: false).balance(); @@ -263,6 +265,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { final availableTypes = ref.giftByTab.entries .where((entry) => entry.value.isNotEmpty) + .where((entry) => entry.key != "CUSTOMIZED") .map((entry) => entry.key) .toList(); final orderedTypes = []; diff --git a/lib/modules/gift/gift_tab_page.dart b/lib/modules/gift/gift_tab_page.dart index 6536a10..574c267 100644 --- a/lib/modules/gift/gift_tab_page.dart +++ b/lib/modules/gift/gift_tab_page.dart @@ -14,8 +14,6 @@ import 'package:yumi/app_localizations.dart'; import 'package:yumi/app/config/business_logic_strategy.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; -import '../../shared/data_sources/models/enum/sc_gift_type.dart'; - enum _GiftGridPageStatus { idle, loading, ready } const Duration _kGiftPageSkeletonMinDuration = Duration(milliseconds: 420); @@ -405,113 +403,63 @@ class _GiftTabPageState extends State }, ) : GestureDetector( - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.w), - color: Colors.white10, - border: Border.all( - color: - checkedIndex == - ref.giftByTab[widget.type]!.indexOf(gift) - ? SocialChatTheme.primaryLight - : Colors.transparent, - width: 1.w, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + color: Colors.white10, + border: Border.all( + color: + checkedIndex == ref.giftByTab[widget.type]!.indexOf(gift) + ? SocialChatTheme.primaryLight + : Colors.transparent, + width: 1.w, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + netImage( + url: gift.giftPhoto ?? "", + fit: BoxFit.cover, + width: 48.w, + height: 48.w, + loadingWidget: _buildGiftCoverLoading(), + errorWidget: _buildGiftCoverNoData(), + ), + SizedBox(height: 5.w), + Container( + height: 23.w, + margin: EdgeInsets.symmetric(horizontal: 3.w), + alignment: Alignment.center, + child: text( + gift.giftName ?? "", + maxLines: 2, + fontSize: 10.sp, + letterSpacing: 0.1, + lineHeight: 1, + textAlign: TextAlign.center, + fontWeight: FontWeight.w600, + textColor: widget.isDark ? Colors.white : Colors.black, ), ), - child: Column( + Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - netImage( - url: gift.giftPhoto ?? "", - fit: BoxFit.cover, - width: 48.w, - height: 48.w, - loadingWidget: _buildGiftCoverLoading(), - errorWidget: _buildGiftCoverNoData(), + Image.asset( + _strategy.getGiftPageGoldCoinIcon(), + width: 14.w, + height: 14.w, ), - SizedBox(height: 5.w), - Container( - height: 23.w, - margin: EdgeInsets.symmetric(horizontal: 3.w), - alignment: Alignment.center, - child: text( - gift.giftName ?? "", - maxLines: 2, - fontSize: 10.sp, - letterSpacing: 0.1, - lineHeight: 1, - textAlign: TextAlign.center, - fontWeight: FontWeight.w600, - textColor: widget.isDark ? Colors.white : Colors.black, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset( - _strategy.getGiftPageGoldCoinIcon(), - width: 14.w, - height: 14.w, - ), - SizedBox(width: 3.w), - text( - "${gift.giftCandy}", - fontSize: 10.sp, - textColor: - widget.isDark ? Colors.white : Colors.black, - ), - ], + SizedBox(width: 3.w), + text( + "${gift.giftCandy}", + fontSize: 10.sp, + textColor: widget.isDark ? Colors.white : Colors.black, ), ], ), - ), - Positioned( - right: 3.w, - child: Column( - spacing: 3.w, - children: [ - SizedBox(height: 3.w), - // 只添加需要的 widget - if (scGiftHasFullScreenEffect(gift.special)) - Image.asset( - _strategy.getGiftPageGiftEffectIcon( - SCGiftType.ANIMSCION.name, - ), - width: 16.w, - height: 16.w, - fit: BoxFit.fill, - ), - if (gift.special!.contains(SCGiftType.MUSIC.name)) - Image.asset( - _strategy.getGiftPageGiftMusicIcon( - SCGiftType.MUSIC.name, - ), - width: 16.w, - height: 16.w, - fit: BoxFit.fill, - ), - if (gift.giftTab == (SCGiftType.LUCKY_GIFT.name)) - Image.asset( - _strategy.getGiftPageGiftLuckIcon( - SCGiftType.LUCKY_GIFT.name, - ), - width: 16.w, - height: 16.w, - fit: BoxFit.fill, - ), - if (gift.giftTab == (SCGiftType.CP.name)) - Image.asset( - _strategy.getGiftPageGiftCpIcon(SCGiftType.CP.name), - width: 16.w, - height: 16.w, - fit: BoxFit.fill, - ), - ], - ), - ), - ], + ], + ), ), onTap: () { setState(() { diff --git a/lib/modules/room/seat/sc_seat_item.dart b/lib/modules/room/seat/sc_seat_item.dart index e3f75ae..77d0c84 100644 --- a/lib/modules/room/seat/sc_seat_item.dart +++ b/lib/modules/room/seat/sc_seat_item.dart @@ -10,7 +10,6 @@ import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; -import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/shared/data_sources/models/enum/sc_room_special_mike_type.dart'; @@ -313,15 +312,18 @@ class _SeatRenderSnapshot { class _SeatEmojiSnapshot { const _SeatEmojiSnapshot({ + required this.eventVersion, required this.userId, required this.emojiPath, required this.type, required this.number, }); - factory _SeatEmojiSnapshot.fromMic(MicRes? mic) { + factory _SeatEmojiSnapshot.fromProvider(RtcProvider provider, num index) { + final mic = provider.micAtIndexForDisplay(index); final emojiPath = mic?.emojiPath ?? ""; return _SeatEmojiSnapshot( + eventVersion: provider.emojiEventVersionForSeat(index), userId: mic?.user?.id ?? "", emojiPath: emojiPath.isEmpty ? null : emojiPath, type: mic?.type ?? "", @@ -329,6 +331,7 @@ class _SeatEmojiSnapshot { ); } + final int eventVersion; final String userId; final String? emojiPath; final String type; @@ -342,6 +345,7 @@ class _SeatEmojiSnapshot { return true; } return other is _SeatEmojiSnapshot && + other.eventVersion == eventVersion && other.userId == userId && other.emojiPath == emojiPath && other.type == type && @@ -349,7 +353,8 @@ class _SeatEmojiSnapshot { } @override - int get hashCode => Object.hash(userId, emojiPath, type, number); + int get hashCode => + Object.hash(eventVersion, userId, emojiPath, type, number); } class _SeatEmojiEvent { @@ -404,13 +409,18 @@ class _EmoticonsState extends State with TickerProviderStateMixin { super.initState(); } + @override + void dispose() { + emoticonsAniCtr.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Selector( selector: - (context, provider) => _SeatEmojiSnapshot.fromMic( - provider.micAtIndexForDisplay(widget.index), - ), + (context, provider) => + _SeatEmojiSnapshot.fromProvider(provider, widget.index), builder: ( BuildContext context, _SeatEmojiSnapshot snapshot, @@ -423,8 +433,10 @@ class _EmoticonsState extends State with TickerProviderStateMixin { pathList.add( _SeatEmojiEvent(type: snapshot.type, number: snapshot.number), ); - context.read().roomWheatMap[widget.index]?.setEmojiPath = - null; + context.read().consumeSeatEmojiEvent( + widget.index, + snapshot.eventVersion, + ); _checkStart(); } if (playingEvent != null) { @@ -518,6 +530,9 @@ class _EmoticonsState extends State with TickerProviderStateMixin { pathList.removeAt(0); emoticonsAniCtr.forward(); Future.delayed(Duration(milliseconds: 5)).then((value) { + if (!mounted) { + return; + } setState(() { showIn = true; }); @@ -525,6 +540,9 @@ class _EmoticonsState extends State with TickerProviderStateMixin { /// 延迟3秒移除表情包 Future.delayed(Duration(seconds: 3)).then((value) { + if (!mounted) { + return; + } emoticonsAniCtr.reverse(); showIn = false; playingEvent = null; @@ -535,6 +553,9 @@ class _EmoticonsState extends State with TickerProviderStateMixin { giftList.removeAt(0); emoticonsAniCtr.forward(); Future.delayed(Duration(milliseconds: 10)).then((value) { + if (!mounted) { + return; + } setState(() { showIn = true; }); @@ -542,6 +563,9 @@ class _EmoticonsState extends State with TickerProviderStateMixin { /// 延迟3秒移除表情包 Future.delayed(Duration(seconds: 1)).then((value) { + if (!mounted) { + return; + } emoticonsAniCtr.reverse(); showIn = false; giftPath = null; diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index 341fcf7..36aeae9 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -71,9 +71,9 @@ class _VoiceRoomPageState extends State if (!mounted) { return; } - context.read().requestMicrophoneListRefresh( - notifyIfUnchanged: true, - ); + final rtcProvider = context.read(); + rtcProvider.requestMicrophoneListRefresh(notifyIfUnchanged: true); + rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: true); }); _subscription = eventBus.on().listen(( diff --git a/lib/modules/user/me_page2.dart b/lib/modules/user/me_page2.dart index 9a5953b..2b47e4b 100644 --- a/lib/modules/user/me_page2.dart +++ b/lib/modules/user/me_page2.dart @@ -224,12 +224,6 @@ class _MePage2State extends State { iconPath: 'sc_images/index/sc_icon_bag.png', onTap: () => SCNavigatorUtils.push(context, StoreRoute.bags), ), - SizedBox(width: 10.w), - _buildEntryItem( - title: SCAppLocalizations.of(context)!.level, - iconPath: 'sc_images/index/sc_icon_level.png', - onTap: () => SCNavigatorUtils.push(context, SCMainRoute.levelList), - ), ], ); } diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index ff108d9..3c00843 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -65,6 +65,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { bool _disableOnlineUsersRefreshForCurrentSession = false; bool _isSelfMicActionInFlight = false; bool _pendingMicListRefresh = false; + bool _pendingMicListRefreshNotifyIfUnchanged = false; Timer? _deferredMicListRefreshTimer; int _lastMicListRefreshStartedAtMs = 0; num? _preferredSelfMicIndex; @@ -86,6 +87,8 @@ class RealTimeCommunicationManager extends ChangeNotifier { ///麦位 Map roomWheatMap = {}; + final Map _seatEmojiEventVersions = {}; + int _emojiPlaybackEventVersion = 0; RtcEngine? engine; RtcEngineEventHandler? _rtcEngineEventHandler; @@ -375,6 +378,18 @@ class RealTimeCommunicationManager extends ChangeNotifier { return seat?.copyWith(clearUser: true); } + int emojiEventVersionForSeat(num index) { + return _seatEmojiEventVersions[index] ?? 0; + } + + void consumeSeatEmojiEvent(num index, int eventVersion) { + if (emojiEventVersionForSeat(index) != eventVersion) { + return; + } + _seatEmojiEventVersions.remove(index); + roomWheatMap[index]?.setEmojiPath = null; + } + Map _stabilizeSelfMicSnapshot( Map nextMap, { required Map previousMap, @@ -1245,6 +1260,8 @@ class RealTimeCommunicationManager extends ChangeNotifier { } if (_isRefreshingMicList) { _pendingMicListRefresh = true; + _pendingMicListRefreshNotifyIfUnchanged = + _pendingMicListRefreshNotifyIfUnchanged || notifyIfUnchanged; return; } _isRefreshingMicList = true; @@ -1279,9 +1296,14 @@ class RealTimeCommunicationManager extends ChangeNotifier { _isRefreshingMicList = false; if (_pendingMicListRefresh) { _pendingMicListRefresh = false; + final pendingNotifyIfUnchanged = + _pendingMicListRefreshNotifyIfUnchanged; + _pendingMicListRefreshNotifyIfUnchanged = false; unawaited( Future.microtask( - () => retrieveMicrophoneList(notifyIfUnchanged: notifyIfUnchanged), + () => retrieveMicrophoneList( + notifyIfUnchanged: notifyIfUnchanged || pendingNotifyIfUnchanged, + ), ), ); } @@ -1377,6 +1399,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { _disableMicListRefreshForCurrentSession = false; _disableOnlineUsersRefreshForCurrentSession = false; _isSelfMicActionInFlight = false; + _pendingMicListRefresh = false; + _pendingMicListRefreshNotifyIfUnchanged = false; + _seatEmojiEventVersions.clear(); + _emojiPlaybackEventVersion = 0; roomRocketStatus = null; rtmProvider ?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] = @@ -1513,8 +1539,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { return false; } - void _refreshMicListSilently() { - retrieveMicrophoneList(notifyIfUnchanged: false).catchError((_) {}); + void _refreshMicListSilently({bool notifyIfUnchanged = false}) { + retrieveMicrophoneList( + notifyIfUnchanged: notifyIfUnchanged, + ).catchError((_) {}); } void _clearUserFromSeats(String? userId, {num? exceptIndex}) { @@ -1634,10 +1662,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { } notifyListeners(); - _refreshMicListSilently(); + _refreshMicListSilently(notifyIfUnchanged: true); } catch (ex) { if (_isCancelledRequest(ex)) { - _refreshMicListSilently(); + _refreshMicListSilently(notifyIfUnchanged: true); return; } SCTts.show('Failed to put on the microphone, ${_errorMessageFrom(ex)}'); @@ -1684,7 +1712,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { ); } catch (ex) { if (_isCancelledRequest(ex)) { - _refreshMicListSilently(); + _refreshMicListSilently(notifyIfUnchanged: true); return; } SCTts.show('Failed to leave the microphone, ${_errorMessageFrom(ex)}'); @@ -1998,16 +2026,20 @@ class RealTimeCommunicationManager extends ChangeNotifier { } void starPlayEmoji(Msg msg) { - if (msg.number! > -1) { - var mic = roomWheatMap[msg.number]; - if (mic != null && mic.user != null) { - roomWheatMap[msg.number!] = mic.copyWith( - emojiPath: msg.msg, - type: msg.type, - number: msg.msg, - ); - notifyListeners(); - } + final seatIndex = msg.number; + if (seatIndex == null || seatIndex < 0) { + return; + } + var mic = roomWheatMap[seatIndex]; + if (mic != null && mic.user != null) { + _emojiPlaybackEventVersion += 1; + _seatEmojiEventVersions[seatIndex] = _emojiPlaybackEventVersion; + roomWheatMap[seatIndex] = mic.copyWith( + emojiPath: msg.msg, + type: msg.type, + number: msg.msg, + ); + notifyListeners(); } } diff --git a/lib/ui_kit/widgets/room/seat/room_seat_widget.dart b/lib/ui_kit/widgets/room/seat/room_seat_widget.dart index 390d190..f637628 100644 --- a/lib/ui_kit/widgets/room/seat/room_seat_widget.dart +++ b/lib/ui_kit/widgets/room/seat/room_seat_widget.dart @@ -65,12 +65,36 @@ class _RoomSeatWidgetState extends State { return _normalizeSeatCount(count); } + int _seatContentVersion(RtcProvider provider) { + var version = provider.roomWheatMap.length; + final micIndexes = + provider.roomWheatMap.keys.toList()..sort((a, b) => a.compareTo(b)); + for (final micIndex in micIndexes) { + final seat = provider.micAtIndexForDisplay(micIndex); + final user = seat?.user; + version = Object.hash( + version, + micIndex, + seat?.micLock ?? false, + seat?.micMute ?? false, + user?.id ?? "", + user?.userAvatar ?? "", + user?.userNickname ?? "", + user?.roles ?? "", + user?.getHeaddress()?.sourceUrl ?? "", + user?.getVIP()?.name ?? "", + ); + } + return version; + } + @override Widget build(BuildContext context) { return Selector( selector: (context, provider) => _RoomSeatLayoutSnapshot( seatCount: _seatCountFromMicIndexes(provider.roomWheatMap.keys), + seatContentVersion: _seatContentVersion(provider), configuredSeatCount: provider.currenRoom?.roomProfile?.roomSetting?.mikeSize ?.toInt() ?? @@ -233,12 +257,14 @@ class _RoomSeatWidgetState extends State { class _RoomSeatLayoutSnapshot { const _RoomSeatLayoutSnapshot({ required this.seatCount, + required this.seatContentVersion, required this.configuredSeatCount, required this.hasCurrentRoom, required this.isExitingCurrentVoiceRoomSession, }); final int seatCount; + final int seatContentVersion; final int configuredSeatCount; final bool hasCurrentRoom; final bool isExitingCurrentVoiceRoomSession; @@ -250,6 +276,7 @@ class _RoomSeatLayoutSnapshot { } return other is _RoomSeatLayoutSnapshot && other.seatCount == seatCount && + other.seatContentVersion == seatContentVersion && other.configuredSeatCount == configuredSeatCount && other.hasCurrentRoom == hasCurrentRoom && other.isExitingCurrentVoiceRoomSession == @@ -259,6 +286,7 @@ class _RoomSeatLayoutSnapshot { @override int get hashCode => Object.hash( seatCount, + seatContentVersion, configuredSeatCount, hasCurrentRoom, isExitingCurrentVoiceRoomSession, diff --git a/test/room_seat_widget_test.dart b/test/room_seat_widget_test.dart index ab814ca..3bc830a 100644 --- a/test/room_seat_widget_test.dart +++ b/test/room_seat_widget_test.dart @@ -2,14 +2,46 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/modules/room/seat/sc_seat_item.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_res.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import 'package:yumi/ui_kit/widgets/room/seat/room_seat_widget.dart'; void main() { + test('starPlayEmoji creates a new event for repeated same emoji path', () { + final rtcProvider = + RealTimeCommunicationManager() + ..roomWheatMap = { + 1: MicRes(micIndex: 1, user: SocialChatUserProfile(id: 'user-1')), + }; + final msg = Msg( + groupId: 'room-1', + msg: 'sc_images/room/emoji/fluent_emoji_01.webp', + type: SCRoomMsgType.emoticons, + number: 1, + ); + + rtcProvider.starPlayEmoji(msg); + final firstVersion = rtcProvider.emojiEventVersionForSeat(1); + + expect(firstVersion, greaterThan(0)); + expect(rtcProvider.roomWheatMap[1]?.emojiPath, msg.msg); + + rtcProvider.consumeSeatEmojiEvent(1, firstVersion); + expect(rtcProvider.roomWheatMap[1]?.emojiPath, isNull); + + rtcProvider.starPlayEmoji(msg); + final secondVersion = rtcProvider.emojiEventVersionForSeat(1); + + expect(secondVersion, greaterThan(firstVersion)); + expect(rtcProvider.roomWheatMap[1]?.emojiPath, msg.msg); + }); + testWidgets( 'RoomSeatWidget falls back to configured mikeSize when mic list is empty', (tester) async {