上下麦/表情 bug

This commit is contained in:
roxy 2026-04-27 11:17:02 +08:00
parent 6db3aa5ae8
commit 39464469ba
8 changed files with 197 additions and 136 deletions

View File

@ -68,7 +68,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
"LUCKY_GIFT",
"CP",
"MAGIC",
"CUSTOMIZED",
"NSCIONAL_FLAG",
];
@ -212,7 +211,10 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
void initState() {
super.initState();
rtcProvider = Provider.of<RtcProvider>(context, listen: false);
Provider.of<SCAppGeneralManager>(context, listen: false).giftList();
Provider.of<SCAppGeneralManager>(
context,
listen: false,
).giftList(includeCustomized: false);
Provider.of<SCAppGeneralManager>(context, listen: false).giftActivityList();
// Provider.of<GeneralProvider>(context, listen: false).giftBackpack();
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
@ -263,6 +265,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
final availableTypes =
ref.giftByTab.entries
.where((entry) => entry.value.isNotEmpty)
.where((entry) => entry.key != "CUSTOMIZED")
.map((entry) => entry.key)
.toList();
final orderedTypes = <String>[];

View File

@ -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<GiftTabPage>
},
)
: 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(() {

View File

@ -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<Emoticons> with TickerProviderStateMixin {
super.initState();
}
@override
void dispose() {
emoticonsAniCtr.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Selector<RtcProvider, _SeatEmojiSnapshot>(
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<Emoticons> with TickerProviderStateMixin {
pathList.add(
_SeatEmojiEvent(type: snapshot.type, number: snapshot.number),
);
context.read<RtcProvider>().roomWheatMap[widget.index]?.setEmojiPath =
null;
context.read<RtcProvider>().consumeSeatEmojiEvent(
widget.index,
snapshot.eventVersion,
);
_checkStart();
}
if (playingEvent != null) {
@ -518,6 +530,9 @@ class _EmoticonsState extends State<Emoticons> 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<Emoticons> 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<Emoticons> 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<Emoticons> with TickerProviderStateMixin {
/// 3
Future.delayed(Duration(seconds: 1)).then((value) {
if (!mounted) {
return;
}
emoticonsAniCtr.reverse();
showIn = false;
giftPath = null;

View File

@ -71,9 +71,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
if (!mounted) {
return;
}
context.read<RtcProvider>().requestMicrophoneListRefresh(
notifyIfUnchanged: true,
);
final rtcProvider = context.read<RtcProvider>();
rtcProvider.requestMicrophoneListRefresh(notifyIfUnchanged: true);
rtcProvider.fetchOnlineUsersList(notifyIfUnchanged: true);
});
_subscription = eventBus.on<SCGiveRoomLuckPageDisposeEvent>().listen((

View File

@ -224,12 +224,6 @@ class _MePage2State extends State<MePage2> {
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),
),
],
);
}

View File

@ -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<num, MicRes> roomWheatMap = {};
final Map<num, int> _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<num, MicRes> _stabilizeSelfMicSnapshot(
Map<num, MicRes> nextMap, {
required Map<num, MicRes> 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<void>.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();
}
}

View File

@ -65,12 +65,36 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
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<RtcProvider, _RoomSeatLayoutSnapshot>(
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<RoomSeatWidget> {
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,

View File

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