bug 修复

This commit is contained in:
roxy 2026-05-09 13:38:27 +08:00
parent 5f79a8b523
commit 23e538cd0c
13 changed files with 281 additions and 128 deletions

View File

@ -16,7 +16,7 @@ class SCVariant1Config implements AppConfig {
@override
String get apiHost => const String.fromEnvironment(
'API_HOST',
defaultValue: 'http://192.168.110.64:1100/',
defaultValue: 'https://jvapi.haiyihy.com/',
); // 线 --dart-define=API_HOST http://192.168.110.64:1100/线https://jvapi.haiyihy.com/
@override

View File

@ -715,6 +715,7 @@ class _SCEditProfilePageState extends State<SCEditProfilePage> {
userProvider?.syncCurrentUserProfile(user.userProfile);
await DataPersistence.clearPendingRegisterRewardDialog();
await DataPersistence.setAwaitRegisterRewardSocket(true);
await DataPersistence.setPendingFirstRegisterRoomGameEvent(true);
if (!mounted) {
return;
}

View File

@ -8,6 +8,7 @@ import 'package:yumi/main.dart' show routeObserver;
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
import 'package:yumi/modules/home/index_home_page.dart';
import 'package:yumi/modules/chat/message/sc_message_page.dart';
import 'package:yumi/modules/user/task/task_page.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:provider/provider.dart';
@ -25,7 +26,6 @@ import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
import '../../ui_kit/components/sc_float_ichart.dart';
import '../../ui_kit/widgets/daily_sign_in/daily_sign_in_dialog.dart';
import '../../ui_kit/widgets/register_reward/register_reward_dialog.dart';
import '../home/popular/event/home_event_page.dart';
import '../user/me_page2.dart';
///
@ -50,6 +50,7 @@ class _SCIndexPageState extends State<SCIndexPage>
bool _hasShownRegisterRewardDialog = false;
bool _isShowingDailySignInDialog = false;
bool _showRegisterRewardAfterDailySignIn = false;
bool _isOpeningFirstRegisterRoomGame = false;
StreamSubscription<RegisterRewardGrantedEvent>? _registerRewardSubscription;
Completer<void>? _registerRewardSocketCompleter;
PageRoute<dynamic>? _routeObserverRoute;
@ -255,7 +256,7 @@ class _SCIndexPageState extends State<SCIndexPage>
}
_pages.add(SCIndexHomePage());
_pages.add(HomeEventPage());
_pages.add(const TaskPage(showBackButton: false));
_pages.add(SCMessagePage());
_pages.add(MePage2());
}
@ -320,7 +321,7 @@ class _SCIndexPageState extends State<SCIndexPage>
svgaPath: "sc_images/index/sc_icon_explore_anim.svga",
fallbackPath: "sc_images/index/sc_icon_explore_en.png",
),
label: SCAppLocalizations.of(context)!.explore,
label: SCAppLocalizations.of(context)!.task,
),
);
@ -427,6 +428,7 @@ class _SCIndexPageState extends State<SCIndexPage>
debugPrint(
'[SignInReward][Home] skip daily sign-in dialog reason=no-data',
);
await _openFirstRegisterRoomGameIfNeeded();
return;
}
debugPrint(
@ -445,6 +447,10 @@ class _SCIndexPageState extends State<SCIndexPage>
_showRegisterRewardAfterDailySignIn = false;
await _showRegisterRewardDialogIfNeeded();
}
if (!mounted) {
return;
}
await _openFirstRegisterRoomGameIfNeeded();
}
Future<void> _showEntryPopupIfNeeded() async {
@ -542,6 +548,30 @@ class _SCIndexPageState extends State<SCIndexPage>
}
}
Future<void> _openFirstRegisterRoomGameIfNeeded() async {
if (_isOpeningFirstRegisterRoomGame ||
!DataPersistence.getPendingFirstRegisterRoomGameEvent()) {
return;
}
if (!mounted) {
return;
}
_isOpeningFirstRegisterRoomGame = true;
try {
await DataPersistence.clearPendingFirstRegisterRoomGameEvent();
if (!mounted) {
return;
}
debugPrint('[FirstRegisterGame] launch first party room random game');
await SCEntryPopupCoordinator.openFirstPartyRoomRandomGame(context);
} catch (error) {
debugPrint('[FirstRegisterGame] launch failed error=$error');
} finally {
_isOpeningFirstRegisterRoomGame = false;
}
}
Widget _buildBottomTabIcon({
required bool active,
required String svgaPath,

View File

@ -7,7 +7,6 @@ import 'package:yumi/app_localizations.dart';
import 'package:yumi/modules/index/main_route.dart';
import 'package:yumi/modules/store/store_route.dart';
import 'package:yumi/modules/user/settings/settings_route.dart';
import 'package:yumi/modules/user/task/task_route.dart';
import 'package:yumi/modules/user/vip/vip_route.dart';
import 'package:yumi/modules/wallet/wallet_route.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
@ -35,7 +34,6 @@ class _MePage2State extends State<MePage2> {
Map<String, SCUserCounterRes> _counterMap = {};
SCVipStatusRes? _vipStatus;
int _taskClaimableCount = 0;
bool _isVipStatusLoading = false;
@override
@ -55,7 +53,6 @@ class _MePage2State extends State<MePage2> {
profileManager.balance();
_loadCounter();
_loadVipStatus();
_loadTaskClaimableCount();
}
Future<void> _loadCounter() async {
@ -101,17 +98,6 @@ class _MePage2State extends State<MePage2> {
}
}
Future<void> _loadTaskClaimableCount() async {
try {
final count = await SCAccountRepository().taskClaimableCount();
if (!mounted) return;
setState(() => _taskClaimableCount = count);
} catch (_) {
if (!mounted) return;
setState(() => _taskClaimableCount = 0);
}
}
Future<SCVipStatusRes> _loadVipEntryStatus() async {
final repository = SCVipRepositoryImp();
try {
@ -335,21 +321,6 @@ class _MePage2State extends State<MePage2> {
onTap: () => SCNavigatorUtils.push(context, StoreRoute.list),
),
SizedBox(width: 10.w),
_buildEntryItem(
title: SCAppLocalizations.of(context)!.task,
iconPath: 'sc_images/index/sc_icon_task.png',
badgeCount: _taskClaimableCount,
onTap: () async {
await SCNavigatorUtils.push(context, TaskRoute.task);
if (!mounted) return;
_loadTaskClaimableCount();
Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
).balance();
},
),
SizedBox(width: 10.w),
_buildEntryItem(
title: SCAppLocalizations.of(context)!.level,
iconPath: 'sc_images/index/sc_icon_level.png',

View File

@ -12,7 +12,9 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository
import 'package:yumi/shared/tools/sc_entry_popup_coordinator.dart';
class TaskPage extends StatefulWidget {
const TaskPage({super.key});
const TaskPage({super.key, this.showBackButton = true});
final bool showBackButton;
@override
State<TaskPage> createState() => _TaskPageState();
@ -201,21 +203,24 @@ class _TaskPageState extends State<TaskPage>
children: [
Align(
alignment: AlignmentDirectional.centerStart,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => SCNavigatorUtils.goBack(context),
child: SizedBox(
width: 44.w,
height: 44.w,
child: Icon(
SCGlobalConfig.lang == 'ar'
? Icons.keyboard_arrow_right
: Icons.keyboard_arrow_left,
color: Colors.white,
size: 28.w,
),
),
),
child:
widget.showBackButton
? GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => SCNavigatorUtils.goBack(context),
child: SizedBox(
width: 44.w,
height: 44.w,
child: Icon(
SCGlobalConfig.lang == 'ar'
? Icons.keyboard_arrow_right
: Icons.keyboard_arrow_left,
color: Colors.white,
size: 28.w,
),
),
)
: SizedBox(width: 44.w, height: 44.w),
),
Text(
SCAppLocalizations.of(context)!.task,

View File

@ -297,6 +297,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
);
static const Duration _agoraJoinTimeout = Duration(seconds: 8);
static const Duration _agoraDisconnectedGracePeriod = Duration(seconds: 12);
static const Duration _roomMusicPublishingStateTtl = Duration(seconds: 15);
static const String _roomStartupSeatLoadingTag =
'roomStartupSeatInteractionLoading';
static const String _selfMicGoUpRetryLoadingTag = 'selfMicGoUpRetryLoading';
@ -366,6 +367,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
final Map<num, int> _seatEmojiEventVersions = {};
int _emojiPlaybackEventVersion = 0;
final Set<String> _roomMusicPublishingUserIds = <String>{};
final Map<String, Timer> _roomMusicPublishingClearTimers = <String, Timer>{};
RtcEngine? engine;
RtcEngineEventHandler? _rtcEngineEventHandler;
@ -1313,11 +1315,15 @@ class RealTimeCommunicationManager extends ChangeNotifier {
.map((seat) => (seat.user?.id ?? "").trim())
.where((userId) => userId.isNotEmpty)
.toSet();
final beforeCount = _roomMusicPublishingUserIds.length;
_roomMusicPublishingUserIds.removeWhere(
(userId) => !seatedUserIds.contains(userId),
);
return beforeCount != _roomMusicPublishingUserIds.length;
final removedUserIds =
_roomMusicPublishingUserIds
.where((userId) => !seatedUserIds.contains(userId))
.toList();
for (final userId in removedUserIds) {
_roomMusicPublishingUserIds.remove(userId);
_roomMusicPublishingClearTimers.remove(userId)?.cancel();
}
return removedUserIds.isNotEmpty;
}
int? _tryParseAgoraUid(String? value) {
@ -3364,7 +3370,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_pendingMicListRefreshNotifyIfUnchanged = false;
_seatEmojiEventVersions.clear();
_emojiPlaybackEventVersion = 0;
_roomMusicPublishingUserIds.clear();
_clearRoomMusicPublishingStates();
roomRocketStatus = null;
rtmProvider
?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] =
@ -3405,6 +3411,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
@override
void dispose() {
_stopRoomStatePolling();
_clearRoomMusicPublishingStates();
unawaited(releaseRtcEngineForAppTermination());
super.dispose();
}
@ -4123,15 +4130,36 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (normalizedUserId.isEmpty) {
return;
}
final changed =
publishing
? _roomMusicPublishingUserIds.add(normalizedUserId)
: _roomMusicPublishingUserIds.remove(normalizedUserId);
bool changed = false;
if (publishing) {
changed = _roomMusicPublishingUserIds.add(normalizedUserId);
_roomMusicPublishingClearTimers.remove(normalizedUserId)?.cancel();
_roomMusicPublishingClearTimers[normalizedUserId] = Timer(
_roomMusicPublishingStateTtl,
() {
_roomMusicPublishingClearTimers.remove(normalizedUserId);
if (_roomMusicPublishingUserIds.remove(normalizedUserId)) {
notifyListeners();
}
},
);
} else {
_roomMusicPublishingClearTimers.remove(normalizedUserId)?.cancel();
changed = _roomMusicPublishingUserIds.remove(normalizedUserId);
}
if (changed && notifyIfChanged) {
notifyListeners();
}
}
void _clearRoomMusicPublishingStates() {
for (final timer in _roomMusicPublishingClearTimers.values) {
timer.cancel();
}
_roomMusicPublishingClearTimers.clear();
_roomMusicPublishingUserIds.clear();
}
void handleRoomMusicPublishingMessage(Msg msg) {
final userId =
(msg.role ?? "").trim().isNotEmpty ? msg.role : msg.user?.id ?? "";
@ -4148,9 +4176,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return;
}
final effectivePublishing = publishing && isOnMai();
updateRoomMusicPublishingState(
userId: currentUserId,
publishing: publishing && isOnMai(),
publishing: effectivePublishing,
);
final groupId =
@ -4158,14 +4187,18 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (groupId.isEmpty) {
return;
}
if (rtmProvider == null && context != null && context!.mounted) {
rtmProvider = Provider.of<RtmProvider>(context!, listen: false);
}
final roomRtmProvider = rtmProvider;
unawaited(
rtmProvider?.dispatchMessage(
roomRtmProvider?.dispatchMessage(
Msg(
groupId: groupId,
msg: publishing && isOnMai() ? "1" : "0",
msg: effectivePublishing ? "1" : "0",
type: SCRoomMsgType.roomMusic,
role: currentUserId,
number: publishing && isOnMai() ? 1 : 0,
number: effectivePublishing ? 1 : 0,
user: currentUser,
),
addLocal: false,

View File

@ -13,6 +13,10 @@ import 'package:yumi/ui_kit/components/sc_tts.dart';
enum RoomMusicPlayMode { list, shuffle, single }
class RoomMusicManager extends ChangeNotifier {
static const Duration _publishingStateHeartbeatInterval = Duration(
seconds: 5,
);
RoomMusicManager({RoomMusicRepository? repository})
: _repository = repository ?? RoomMusicRepository() {
_playlist = _repository.loadSongs();
@ -23,6 +27,7 @@ class RoomMusicManager extends ChangeNotifier {
RtcProvider? _rtcProvider;
Timer? _positionTimer;
Timer? _publishingStateHeartbeatTimer;
List<SCMusicMode> _playlist = <SCMusicMode>[];
SCMusicMode? _current;
RoomMusicPlayMode _playMode = RoomMusicPlayMode.list;
@ -489,9 +494,11 @@ class RoomMusicManager extends ChangeNotifier {
void _syncPublishingState({bool force = false}) {
final rtcProvider = _rtcProvider;
if (rtcProvider == null) {
_updatePublishingStateHeartbeat(false);
return;
}
final publishing = isPublishingToRoom;
_updatePublishingStateHeartbeat(publishing);
if (!force && publishing == _lastPublishedRoomState) {
return;
}
@ -499,6 +506,29 @@ class RoomMusicManager extends ChangeNotifier {
rtcProvider.publishCurrentUserRoomMusicState(publishing);
}
void _updatePublishingStateHeartbeat(bool publishing) {
if (!publishing) {
_publishingStateHeartbeatTimer?.cancel();
_publishingStateHeartbeatTimer = null;
return;
}
if (_publishingStateHeartbeatTimer != null) {
return;
}
_publishingStateHeartbeatTimer = Timer.periodic(
_publishingStateHeartbeatInterval,
(_) {
if (!isPublishingToRoom) {
_publishingStateHeartbeatTimer?.cancel();
_publishingStateHeartbeatTimer = null;
_syncPublishingState(force: true);
return;
}
_rtcProvider?.publishCurrentUserRoomMusicState(true);
},
);
}
void _restoreCurrentAfterPlaylistChange() {
final currentId = _current?.id;
if (currentId == null) {
@ -522,6 +552,7 @@ class RoomMusicManager extends ChangeNotifier {
@override
void dispose() {
_positionTimer?.cancel();
_publishingStateHeartbeatTimer?.cancel();
_rtcProvider?.setRoomMusicMixingStateListener(null);
_rtcProvider?.setRoomMusicMicRouteChangedListener(null);
super.dispose();

View File

@ -10,6 +10,8 @@ class DataPersistence {
"await_register_reward_socket";
static const String _pendingRegisterRewardDialogKey =
"pending_register_reward_dialog";
static const String _pendingFirstRegisterRoomGameEventKey =
"pending_first_register_room_game_event";
static const String _pendingChannelAuthTypeKey = "pending_channel_auth_type";
static const String _pendingChannelAuthOpenIdKey =
"pending_channel_auth_open_id";
@ -218,6 +220,18 @@ class DataPersistence {
return await remove(_awaitRegisterRewardSocketKey);
}
static Future<bool> setPendingFirstRegisterRoomGameEvent(bool value) async {
return await setBool(_pendingFirstRegisterRoomGameEventKey, value);
}
static bool getPendingFirstRegisterRoomGameEvent() {
return getBool(_pendingFirstRegisterRoomGameEventKey);
}
static Future<bool> clearPendingFirstRegisterRoomGameEvent() async {
return await remove(_pendingFirstRegisterRoomGameEventKey);
}
static Future<void> setPendingChannelAuth(
String authType,
String openId,

View File

@ -13,6 +13,8 @@ import 'package:yumi/modules/user/task/task_route.dart';
import 'package:yumi/modules/user/vip/vip_route.dart';
import 'package:yumi/modules/wallet/wallet_route.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/home/home_room_preload_manager.dart';
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_entry_popup_res.dart';
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
@ -288,6 +290,7 @@ class SCEntryPopupCoordinator {
onSelectRootTab?.call(0);
return;
case '/tab/explore':
case '/tab/task':
onSelectRootTab?.call(1);
return;
case '/tab/message':
@ -402,12 +405,49 @@ class SCEntryPopupCoordinator {
);
}
static Future<bool> _enterRoom(BuildContext context, String roomId) async {
static Future<void> openFirstPartyRoomRandomGame(BuildContext context) async {
if (!context.mounted) {
return;
}
final rooms = await SCHomeRoomPreloadManager.instance.loadPartyRooms();
final room = _firstRoomWithId(rooms);
final roomId = room?.id?.trim() ?? '';
if (room == null || roomId.isEmpty || !context.mounted) {
_log('first-register-game skip reason=no-room');
return;
}
_log('first-register-game enter roomId=$roomId');
final ready = await _enterRoom(
context,
roomId,
previewData: RoomEntryPreviewData.fromSocialChatRoom(room),
);
if (!ready || !context.mounted) {
_log('first-register-game skip reason=room-not-ready roomId=$roomId');
return;
}
showBottomInBottomDialog(
context,
RoomGameBottomSheet(roomContext: context, initialGameId: '0'),
barrierColor: Colors.black54,
);
}
static Future<bool> _enterRoom(
BuildContext context,
String roomId, {
RoomEntryPreviewData? previewData,
}) async {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
RoomEntryPreviewData? previewData;
RoomEntryPreviewData? resolvedPreviewData = previewData;
try {
final room = await SCChatRoomRepository().specific(roomId);
previewData = RoomEntryPreviewData.fromMyRoom(room);
if (resolvedPreviewData == null) {
final room = await SCChatRoomRepository().specific(roomId);
resolvedPreviewData = RoomEntryPreviewData.fromMyRoom(room);
}
} catch (error) {
debugPrint(
'[EntryPopup] load room preview failed roomId=$roomId: $error',
@ -416,10 +456,25 @@ class SCEntryPopupCoordinator {
if (!context.mounted) {
return false;
}
rtcProvider.joinVoiceRoomSession(context, roomId, previewData: previewData);
rtcProvider.joinVoiceRoomSession(
context,
roomId,
previewData: resolvedPreviewData,
);
return _waitForRoomReady(rtcProvider, roomId);
}
static SocialChatRoomRes? _firstRoomWithId(
Iterable<SocialChatRoomRes> rooms,
) {
for (final room in rooms) {
if ((room.id ?? '').trim().isNotEmpty) {
return room;
}
}
return null;
}
static Future<bool> _waitForRoomReady(
RtcProvider rtcProvider,
String roomId,

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_debouncer/flutter_debouncer.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
import 'package:yumi/shared/tools/sc_room_utils.dart';
import 'package:yumi/main.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
@ -324,11 +323,7 @@ class _FloatingRoomRedenvelopeScreenWidgetState
}
return;
}
if ((currentRoomId ?? '').trim().isEmpty || currentRoomId == roomId) {
rtcProvider.joinVoiceRoomSession(currentContext, roomId);
return;
}
SCRoomUtils.goRoom(roomId, currentContext, fromFloting: true);
rtcProvider.joinVoiceRoomSession(currentContext, roomId);
}
String _senderNameForFloating() {

View File

@ -324,27 +324,10 @@ class _ClaimAvatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ClipOval(
child: Container(
width: 34.w,
height: 34.w,
color: Colors.white.withValues(alpha: 0.08),
alignment: Alignment.center,
child:
url.trim().isNotEmpty
? netImage(
url: url,
width: 34.w,
height: 34.w,
fit: BoxFit.cover,
)
: Image.asset(
'sc_images/room/red_packet/small_packet_icon.png',
width: 24.w,
height: 24.w,
fit: BoxFit.contain,
),
),
return SizedBox(
width: 34.w,
height: 34.w,
child: head(url: url, width: 34.w, showDefault: true),
);
}
}

View File

@ -197,32 +197,56 @@ class _MsgItemState extends State<MsgItem> {
Widget _buildTextMessageBubble() {
final chatBubble = widget.msg.user?.getChatBox();
final hasChatBubble = _resolveRoomChatBubbleImageUrl(chatBubble).isNotEmpty;
if (hasChatBubble) {
return _buildVipTextMessageBubble(chatBubble);
}
final bubbleStartMargin = 54.w;
return LayoutBuilder(
builder: (context, constraints) {
final rowMaxWidth =
constraints.maxWidth.isFinite
? constraints.maxWidth
: ScreenUtil().screenWidth;
final availableBubbleWidth = math.max(
1.0,
rowMaxWidth - bubbleStartMargin,
);
if (hasChatBubble) {
return _buildVipTextMessageBubble(
chatBubble,
maxBubbleWidth: availableBubbleWidth,
startMargin: bubbleStartMargin,
);
}
return Container(
constraints: BoxConstraints(maxWidth: ScreenUtil().screenWidth * 0.7),
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.only(
topRight: Radius.circular(15.w),
bottomLeft: Radius.circular(15.w),
bottomRight: Radius.circular(15.w),
),
),
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
margin: EdgeInsetsDirectional.only(start: 54.w).copyWith(bottom: 8.w),
child: ExtendedText(
widget.msg.msg ?? '',
style: TextStyle(
fontSize: sp(13),
color: Color(_msgColor(widget.msg.type ?? "")),
fontWeight: FontWeight.w500,
),
softWrap: true,
specialTextSpanBuilder: AtTextSpanBuilder(onTapCall: (userId) {}),
),
return Container(
constraints: BoxConstraints(
maxWidth: math.min(
ScreenUtil().screenWidth * 0.7,
availableBubbleWidth,
),
),
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.only(
topRight: Radius.circular(15.w),
bottomLeft: Radius.circular(15.w),
bottomRight: Radius.circular(15.w),
),
),
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
margin: EdgeInsetsDirectional.only(
start: bubbleStartMargin,
).copyWith(bottom: 8.w),
child: ExtendedText(
widget.msg.msg ?? '',
style: TextStyle(
fontSize: sp(13),
color: Color(_msgColor(widget.msg.type ?? "")),
fontWeight: FontWeight.w500,
),
softWrap: true,
specialTextSpanBuilder: AtTextSpanBuilder(onTapCall: (userId) {}),
),
);
},
);
}
@ -255,7 +279,11 @@ class _MsgItemState extends State<MsgItem> {
);
}
Widget _buildVipTextMessageBubble(PropsResources? resource) {
Widget _buildVipTextMessageBubble(
PropsResources? resource, {
required double maxBubbleWidth,
required double startMargin,
}) {
final messageText = widget.msg.msg ?? "";
final textStyle = TextStyle(
fontSize: sp(13),
@ -263,10 +291,7 @@ class _MsgItemState extends State<MsgItem> {
fontWeight: FontWeight.w500,
);
final imageUrl = _resolveRoomChatBubbleImageUrl(resource);
final multilineMaxBubbleWidth = math.min(
ScreenUtil().screenWidth * 0.72,
300.w,
);
final multilineMaxBubbleWidth = math.min(maxBubbleWidth, 300.w);
final multilineMaxTextWidth = math.max(
82.w,
multilineMaxBubbleWidth - 64.w,
@ -282,6 +307,7 @@ class _MsgItemState extends State<MsgItem> {
imageUrl: imageUrl,
textSize: multilineTextPainter.size,
maxBubbleWidth: multilineMaxBubbleWidth,
startMargin: startMargin,
);
}
@ -289,6 +315,8 @@ class _MsgItemState extends State<MsgItem> {
messageText: messageText,
textStyle: textStyle,
imageUrl: imageUrl,
maxBubbleWidth: maxBubbleWidth,
startMargin: startMargin,
);
}
@ -298,6 +326,7 @@ class _MsgItemState extends State<MsgItem> {
required String imageUrl,
required Size textSize,
required double maxBubbleWidth,
required double startMargin,
}) {
final leftPadding = 22.w;
final rightPadding = 42.w;
@ -323,7 +352,9 @@ class _MsgItemState extends State<MsgItem> {
return Container(
width: bubbleWidth,
height: bubbleHeight,
margin: EdgeInsetsDirectional.only(start: 54.w).copyWith(bottom: 8.w),
margin: EdgeInsetsDirectional.only(
start: startMargin,
).copyWith(bottom: 8.w),
child: Stack(
fit: StackFit.expand,
children: [
@ -353,6 +384,8 @@ class _MsgItemState extends State<MsgItem> {
required String messageText,
required TextStyle textStyle,
required String imageUrl,
required double maxBubbleWidth,
required double startMargin,
}) {
final leftPadding = 22.w;
final rightPadding = 24.w;
@ -361,7 +394,6 @@ class _MsgItemState extends State<MsgItem> {
final minSkinWidth = 96.w;
final minSkinHeight = 58.w;
final textWidthSafety = 10.w;
final maxBubbleWidth = ScreenUtil().screenWidth * 0.7;
final maxTextWidth = math.max(
1.0,
maxBubbleWidth - leftPadding - rightPadding,
@ -390,7 +422,9 @@ class _MsgItemState extends State<MsgItem> {
return Container(
width: bubbleWidth,
height: bubbleHeight,
margin: EdgeInsetsDirectional.only(start: 54.w).copyWith(bottom: 8.w),
margin: EdgeInsetsDirectional.only(
start: startMargin,
).copyWith(bottom: 8.w),
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,

View File

@ -556,6 +556,7 @@
- 已修正两个红包联调问题:当前房间点击红包飘窗 `GO` 打开领取层时会合并房间缓存中的完整红包 payload避免限时红包丢失 `claimStartTime/countdown`;领取成功后发送的房内 IM 文本会附带不展示的唯一业务 key避免同一用户连续领取同一发包人的多个红包时公屏只保留一条领取记录。
- 已收口红包发送记录页状态展示:不再显示 `Pending/等待中`,后端 `ACTIVE/PENDING` 等中间态统一展示为已发出;只保留已退回、已领完、已发出三类状态,并补齐多语言的“已领完”文案。
- 已收口房间外红包领取层触发:红包飘屏 `GO` 只有在目标房间页面当前可见时才直接打开领取层,否则先进房;同语区或房间 IM 收到即时红包时,也只有当前可见房间匹配目标 `roomId` 才自动弹领取层,避免首页、房间列表、我的页直接弹抢红包。
- 已按最新反馈修正领取记录头像和跨房进房:领取记录空头像不再回退成红包图标,统一走项目默认头像;红包飘窗 `GO` 跨房点击直接进目标房间,不再弹进入房间二次确认。
## 已知问题
- 已接入语音房表情包素材第一版:桌面 `表情素材` 中的 `fluent_emoji``monkey` gif 已纳入项目资源,底部聊天入口改为图稿里的短白色胶囊并移除内部输入图标,右侧新增独立表情按钮;同一个房间输入弹层现在可从表情按钮直接打开表情包面板,原 unicode emoji 面板已替换为本地 gif 表情。发送逻辑同步按最新规则分流:用户在麦上发表情时只在自己麦位播放,不进入聊天列表;用户未上麦发表情时进入房间聊天框展示,不触发麦位动画。