fix: update room rocket launch flow

This commit is contained in:
zhx 2026-05-16 01:03:35 +08:00
parent ccb5579a85
commit e5fdb78fd6
13 changed files with 519 additions and 459 deletions

View File

@ -21,6 +21,7 @@ import 'package:yumi/ui_kit/components/sc_tts.dart';
import 'package:yumi/app/routes/sc_routes.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/shared/tools/sc_loading_manager.dart';
import 'package:yumi/shared/tools/sc_pick_utils.dart';
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_gift_repository_imp.dart';
@ -60,6 +61,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
bool isLoading = true;
bool isBlacklistLoading = true;
bool isBlacklist = false;
bool _isBackgroundUpdating = false;
Map<String, SCUserCounterRes> counterMap = {};
List<SocialChatGiftRes> giftWallList = [];
@ -582,22 +584,25 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
),
items:
backgroundPhotos.map((item) {
return GestureDetector(
child: netImage(
return _buildProfileBackgroundTapTarget(
ref,
netImage(
url: item.url ?? "",
width: ScreenUtil().screenWidth,
height: 300.w,
fit: BoxFit.cover,
),
onTap: () {},
);
}).toList(),
)
: Image.asset(
'sc_images/person/sc_icon_profile_card_default_bg.png',
width: ScreenUtil().screenWidth,
height: 300.w,
fit: BoxFit.cover,
: _buildProfileBackgroundTapTarget(
ref,
Image.asset(
'sc_images/person/sc_icon_profile_card_default_bg.png',
width: ScreenUtil().screenWidth,
height: 300.w,
fit: BoxFit.cover,
),
),
),
Positioned(
@ -972,6 +977,79 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
);
}
Widget _buildProfileBackgroundTapTarget(
SocialChatUserProfileManager ref,
Widget child,
) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap:
widget.isMe == "true"
? () => _pickAndUpdateProfileBackground(ref)
: null,
child: child,
);
}
void _pickAndUpdateProfileBackground(SocialChatUserProfileManager ref) {
if (_isBackgroundUpdating) {
return;
}
SCPickUtils.pickImage(
context,
(bool success, String url) {
final imageUrl = url.trim();
if (!success || imageUrl.isEmpty || !mounted) {
return;
}
_updateProfileBackground(ref, imageUrl);
},
aspectRatio: ScreenUtil().screenWidth / 300.w,
backOriginalFile: false,
);
}
Future<void> _updateProfileBackground(
SocialChatUserProfileManager ref,
String imageUrl,
) async {
if (_isBackgroundUpdating) {
return;
}
_isBackgroundUpdating = true;
SCLoadingManager.show(context: context);
try {
final updatedProfile = await SCAccountRepository().updateUserInfo(
backgroundPhotos: [imageUrl],
);
final returnedPhotos = updatedProfile.backgroundPhotos ?? [];
final hasUploadedPhoto = returnedPhotos.any(
(photo) => (photo.url ?? "").trim() == imageUrl,
);
final backgroundPhotos =
hasUploadedPhoto
? returnedPhotos
: [PersonPhoto(url: imageUrl, status: 1)];
final mergedProfile = updatedProfile.copyWith(
backgroundPhotos: backgroundPhotos,
);
ref.userProfile = mergedProfile;
ref.syncCurrentUserProfile(mergedProfile);
if (!mounted) {
return;
}
setState(() {});
SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful);
} catch (e) {
if (mounted) {
SCTts.show("update fail $e");
}
} finally {
_isBackgroundUpdating = false;
SCLoadingManager.hide();
}
}
Future<void> _openChat() async {
final conversation = V2TimConversation(
type: ConversationType.V2TIM_C2C,

View File

@ -1,6 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_badge_res.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/components/text/sc_text.dart';
class ProfileWallOfHonorsPage extends StatefulWidget {
@ -18,11 +23,55 @@ class _ProfileWallOfHonorsPageState extends State<ProfileWallOfHonorsPage> {
static const Color _mutedText = Color(0xff8F9A98);
int _selectedTabIndex = 0;
final int _honorCount = 0;
final int _eventCount = 0;
bool _loading = true;
SCUserBadgeRes _badges = SCUserBadgeRes.empty();
List<WearBadge> get _honorBadges =>
_badges.displayBadges.where(_isHonorBadge).toList();
List<WearBadge> get _eventBadges =>
_badges.displayBadges.where(_isEventBadge).toList();
int get _honorCount => _honorBadges.length;
int get _eventCount => _eventBadges.length;
int get _totalCount => _honorCount + _eventCount;
@override
void initState() {
super.initState();
_loadBadges();
}
Future<void> _loadBadges() async {
final userId = widget.tageId.trim();
if (userId.isEmpty) {
setState(() {
_loading = false;
_badges = SCUserBadgeRes.empty();
});
return;
}
try {
final currentUserId =
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
final result =
currentUserId.isNotEmpty && currentUserId == userId
? await SCAccountRepository().currentUserBadges()
: await SCAccountRepository().otherUserBadges(userId);
if (!mounted) return;
setState(() {
_badges = result;
_loading = false;
});
} catch (_) {
if (!mounted) return;
setState(() {
_badges = SCUserBadgeRes.empty(userId: userId);
_loading = false;
});
}
}
Widget _buildTopBar() {
return SizedBox(
width: double.infinity,
@ -131,6 +180,106 @@ class _ProfileWallOfHonorsPageState extends State<ProfileWallOfHonorsPage> {
);
}
Widget _buildLoadingState() {
return const SliverFillRemaining(
hasScrollBody: false,
child: Center(
child: SizedBox(
width: 28,
height: 28,
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
),
),
);
}
Widget _buildBadgeGrid() {
final list = _selectedTabIndex == 0 ? _honorBadges : _eventBadges;
if (list.isEmpty) {
return _buildEmptyState();
}
return SliverPadding(
padding: EdgeInsets.fromLTRB(16.w, 4.w, 16.w, 32.w),
sliver: SliverGrid(
delegate: SliverChildBuilderDelegate(
(context, index) => _buildBadgeItem(list[index]),
childCount: list.length,
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisExtent: 132.w,
mainAxisSpacing: 14.w,
crossAxisSpacing: 12.w,
),
),
);
}
Widget _buildBadgeItem(WearBadge badge) {
final url = _badgeUrl(badge);
final name = (badge.badgeName ?? "").trim();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 86.w,
height: 86.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: const Color(0xff0E332D),
borderRadius: BorderRadius.circular(8.w),
border: Border.all(
color: const Color(0xff18F2B1).withValues(alpha: 0.24),
),
),
child: netImage(
url: url,
width: 68.w,
height: 68.w,
fit: BoxFit.contain,
noDefaultImg: true,
),
),
SizedBox(height: 8.w),
text(
name.isEmpty ? "--" : name,
fontSize: 12,
textColor: Colors.white,
fontWeight: FontWeight.w400,
maxLines: 2,
textAlign: TextAlign.center,
),
],
);
}
bool _isHonorBadge(WearBadge badge) {
final sourceType = badge.sourceType?.trim().toUpperCase() ?? "";
final type = badge.type?.trim().toUpperCase() ?? "";
return sourceType == "ACHIEVEMENT" ||
sourceType == "VIP" ||
(sourceType.isEmpty &&
(type == "LONG" || type == "ACHIEVEMENT" || type == "VIP"));
}
bool _isEventBadge(WearBadge badge) {
final sourceType = badge.sourceType?.trim().toUpperCase() ?? "";
final type = badge.type?.trim().toUpperCase() ?? "";
if (sourceType == "ACTIVITY") {
return true;
}
if (sourceType.isNotEmpty) {
return false;
}
return type == "ACTIVITY" ||
type == "SHORT" ||
(type != "LONG" && type != "VIP" && type != "ACHIEVEMENT");
}
String _badgeUrl(WearBadge badge) {
return (badge.selectUrl ?? "").trim();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -143,7 +292,7 @@ class _ProfileWallOfHonorsPageState extends State<ProfileWallOfHonorsPage> {
SliverToBoxAdapter(
child: Column(children: [_buildTopBar(), _buildTabs()]),
),
_buildEmptyState(),
_loading ? _buildLoadingState() : _buildBadgeGrid(),
],
),
),

View File

@ -42,6 +42,8 @@ class RoomRtcAudioMixingConfig {
abstract class RoomRtcEngineAdapter {
RoomRtcEngineKind get kind;
bool get isInRoom;
Future<void> prewarm();
Future<void> enterRoomAsAudience(RoomRtcEnterRoomConfig config);

View File

@ -345,7 +345,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Timer? _roomRocketLaunchAnimationTimer;
Timer? _roomRocketGiftRefreshTimer;
final List<Timer> _roomRocketPostLaunchRefreshTimers = [];
String? _lastRoomRocketLaunchNoticeKey;
Timer? _roomRedPacketPresenceTimer;
Future<void>? _rtcEnginePrewarmTask;
Future<void>? _pendingRoomSwitchRtcLeaveTask;
@ -1061,6 +1060,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_resetLocalAudioRuntimeTracking();
}
bool get _hasCurrentRoomRtcConnection {
final adapter = _roomRtcEngineAdapter;
return _roomRtcJoined && adapter != null && adapter.isInRoom;
}
bool get _canReportMicActive {
return _roomRtcJoined && _roomRtcRoleIsBroadcaster && isOnMai();
}
@ -1507,6 +1511,42 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
}
Future<void> _rejoinCurrentRoomRtcForSameRoomReuse(String roomId) async {
if (_hasCurrentRoomRtcConnection) {
_setRoomStartupReady();
return;
}
try {
final trtcUserSig = await _fetchRoomRtcJoinCredentialForCurrentRoom();
if (!_isCurrentRoomId(roomId)) {
return;
}
await joinRoomRtcVoiceChannel(
throwOnError: true,
trtcUserSig: trtcUserSig,
);
if (!_isCurrentRoomId(roomId)) {
await _cleanupRoomRtcState(
clearMicSeats: currenRoom == null,
onlyIfBusinessRoomId: roomId,
);
return;
}
await _bootstrapRoomHeartbeatState();
if (!_isCurrentRoomId(roomId)) {
return;
}
_setRoomStartupReady();
} catch (error) {
if (!_isCurrentRoomId(roomId)) {
return;
}
_stopVoiceRoomForegroundService();
_setRoomStartupFailed(RoomStartupFailureType.rtc);
}
}
Future<bool> _switchRoomRtcToAudience() async {
try {
final switched = await _currentRoomRtcEngineAdapter.switchToAudience();
@ -1779,12 +1819,20 @@ class RealTimeCommunicationManager extends ChangeNotifier {
retrieveMicrophoneList();
fetchOnlineUsersList();
_startRoomStatePolling();
_setRoomStartupReady();
final shouldRejoinRoomRtc = !_hasCurrentRoomRtcConnection;
if (shouldRejoinRoomRtc) {
_setRoomStartupLoading();
} else {
_setRoomStartupReady();
}
setRoomVisualEffectsEnabled(true);
_startRoomRedPacketPresenceHeartbeat(roomId);
_refreshRoomRedPacketListAfterEntry(roomId: roomId);
_startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context);
if (shouldRejoinRoomRtc) {
unawaited(_rejoinCurrentRoomRtcForSameRoomReuse(roomId));
}
} else {
JoinRoomRes? preEnteredRoom;
if (pwd == null && _previewRoomRequiresPassword(context, previewData)) {
@ -2823,10 +2871,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
///
void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) {
final previousStatus = roomRocketStatus;
roomRocketStatus = res;
notifyListeners();
_publishRoomRocketLaunchNoticeIfNeeded(previousStatus, res);
_scheduleRoomRocketAssetPreload(
(res.roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(),
res,
@ -2845,10 +2891,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) {
return;
}
final previousStatus = roomRocketStatus;
roomRocketStatus = res;
notifyListeners();
_publishRoomRocketLaunchNoticeIfNeeded(previousStatus, res);
_scheduleRoomRocketAssetPreload(resolvedRoomId, res);
}
@ -2966,114 +3010,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return null;
}
void _publishRoomRocketLaunchNoticeIfNeeded(
SCRoomRocketStatusRes? previous,
SCRoomRocketStatusRes current,
) {
if (!_shouldPublishRoomRocketLaunchNotice(previous, current)) {
return;
}
final roomId =
(current.roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "")
.trim();
final groupId =
(currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "").trim();
if (roomId.isEmpty || groupId.isEmpty) {
return;
}
final level = _roomRocketNoticeLevel(current);
final noticeKey = _roomRocketLaunchNoticeKey(
roomId: roomId,
level: level,
roundNo: current.roundNo ?? 0,
);
if (_lastRoomRocketLaunchNoticeKey == noticeKey) {
return;
}
_lastRoomRocketLaunchNoticeKey = noticeKey;
rtmProvider?.addMsg(
Msg(
groupId: groupId,
msg: _roomRocketLaunchNoticeSeconds(current).toString(),
type: SCRoomMsgType.rocketLaunchNotice,
number: _roomRocketLaunchNoticeSeconds(current),
rocketIconUrl: _rocketIconUrlForStatus(current, level),
giftBatchId: noticeKey,
),
);
}
bool _shouldPublishRoomRocketLaunchNotice(
SCRoomRocketStatusRes? previous,
SCRoomRocketStatusRes current,
) {
if (previous == null) {
return false;
}
final currentRoomId =
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
if (currentRoomId.isNotEmpty &&
(current.roomId ?? currentRoomId).trim() != currentRoomId) {
return false;
}
return _roomRocketStatusPercent(previous) < 99.5 &&
_roomRocketStatusPercent(current) >= 99.5;
}
double _roomRocketStatusPercent(SCRoomRocketStatusRes? status) {
if (status == null) {
return 0;
}
final value = status.displayPercent ?? status.energyPercent;
if (value != null) {
final raw = value.toDouble();
final percent = raw > 0 && raw <= 1 ? raw * 100 : raw;
return percent.clamp(0, 100).toDouble();
}
final currentEnergy = status.currentEnergy ?? 0;
final maxEnergy = status.maxEnergy ?? status.needEnergy ?? 0;
if (maxEnergy <= 0) {
return 0;
}
return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble();
}
int _roomRocketNoticeLevel(SCRoomRocketStatusRes status) {
final level = status.currentLevel ?? status.level?.toInt() ?? 1;
return level.clamp(1, 99).toInt();
}
int _roomRocketLaunchNoticeSeconds(SCRoomRocketStatusRes status) {
final seconds = status.resetRemainingSeconds ?? 0;
if (seconds > 0 && seconds <= 60) {
return seconds;
}
return 10;
}
String _rocketIconUrlForStatus(SCRoomRocketStatusRes status, int level) {
for (final item in status.levels ?? const []) {
if (item.level == level && item.rocketIconUrl.trim().isNotEmpty) {
return item.rocketIconUrl;
}
}
for (final item in roomRocketStatus?.levels ?? const []) {
if (item.level == level && item.rocketIconUrl.trim().isNotEmpty) {
return item.rocketIconUrl;
}
}
return "";
}
String _roomRocketLaunchNoticeKey({
required String roomId,
required int level,
required int roundNo,
}) {
final roundKey = roundNo > 0 ? roundNo.toString() : 'full';
return ['rocket_launch_notice', roomId, level, roundKey].join('|');
}
void _playRoomRocketLaunchAnimation({
required String animationUrl,
required RoomRocketLaunchBroadcastMessage launch,
@ -3680,7 +3616,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomRocketGiftRefreshTimer?.cancel();
_roomRocketGiftRefreshTimer = null;
_cancelRoomRocketPostLaunchStatusRefresh();
_lastRoomRocketLaunchNoticeKey = null;
RoomRocketAssetPreloader.cancelCurrentRoom();
_stopRoomRedPacketPresenceHeartbeat();
_previewRoomSeatCount = null;

View File

@ -121,6 +121,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
static const int _roomRocketLaunchRecentTtlMs = 90000;
static const int _roomRocketLaunchLooseRecentTtlMs = 12000;
static const int _roomRocketRegionBroadcastRecentTtlMs = 90000;
static const Duration _createRoomGroupTimeout = Duration(seconds: 12);
static const List<Duration> _roomRocketRewardPopupRetryDelays = [
Duration(milliseconds: 700),
Duration(seconds: 2),
@ -654,11 +655,20 @@ class RealTimeMessagingManager extends ChangeNotifier {
String groupID,
String groupName,
) {
return V2TIMGroupManager().createGroup(
groupID: groupID,
groupType: GroupType.AVChatRoom,
groupName: groupName,
);
return V2TIMGroupManager()
.createGroup(
groupID: groupID,
groupType: GroupType.AVChatRoom,
groupName: groupName,
)
.timeout(
_createRoomGroupTimeout,
onTimeout:
() => V2TimValueCallback<String>(
code: -10001,
desc: 'Create IM group timeout',
),
);
}
///im群聊
@ -1931,28 +1941,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
currentContext,
listen: false,
);
final isCurrentVisibleRoom =
rtcProvider.shouldShowRoomVisualEffects &&
rtcProvider.isVoiceRoomRouteVisible;
final launch = _buildRoomRocketLaunchFromStatusPayload(
rtcProvider: rtcProvider,
payload: data,
roomId: roomId,
);
final shouldHandleLaunch = _shouldHandleRoomRocketLaunchFromStatusPayload(
rtcProvider: rtcProvider,
payload: data,
launch: launch,
);
if (shouldHandleLaunch) {
_publishRoomRocketRegionBroadcastIfNeeded(launch);
if (isCurrentVisibleRoom) {
_handleCurrentVisibleRoomRocketLaunch(
rtcProvider: rtcProvider,
launch: launch,
);
}
}
unawaited(rtcProvider.refreshRoomRocketStatus(roomId: roomId));
}
@ -2330,141 +2318,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
return ['rocket_launch_loose', launch.roomId, launch.safeLevel].join('|');
}
bool _shouldHandleRoomRocketLaunchFromStatusPayload({
required RealTimeCommunicationManager rtcProvider,
required Map<String, dynamic> payload,
required RoomRocketLaunchBroadcastMessage launch,
}) {
final currentPercent = _roomRocketPayloadPercent(payload);
if (currentPercent < 99.5) {
return false;
}
final previousPercent = _roomRocketStatusPercent(
rtcProvider.roomRocketStatus,
);
if (previousPercent < 99.5) {
return true;
}
final launchKey = _roomRocketLaunchNoticeKey(launch);
if (launchKey.isEmpty) {
return false;
}
_pruneRecentRoomRocketLaunches(DateTime.now().millisecondsSinceEpoch);
final looseLaunchKey = _roomRocketLaunchLooseKey(launch);
return !_recentRoomRocketLaunchTimes.containsKey(launchKey) &&
(looseLaunchKey.isEmpty ||
!_recentRoomRocketLaunchLooseTimes.containsKey(looseLaunchKey));
}
RoomRocketLaunchBroadcastMessage _buildRoomRocketLaunchFromStatusPayload({
required RealTimeCommunicationManager rtcProvider,
required Map<String, dynamic> payload,
required String roomId,
}) {
final roomProfile = rtcProvider.currenRoom?.roomProfile?.roomProfile;
final level = _roomRocketPayloadLevel(
payload,
rtcProvider.roomRocketStatus?.currentLevel ??
rtcProvider.roomRocketStatus?.level?.toInt() ??
1,
);
return RoomRocketLaunchBroadcastMessage(
roomId: roomId,
roomAccount: _firstNonBlank([
roomProfile?.roomAccount,
_payloadText(payload['roomAccount'] ?? payload['room_account']),
]),
roomName: _firstNonBlank([
roomProfile?.roomName,
_payloadText(payload['roomName'] ?? payload['room_name']),
]),
roomCoverUrl: _firstNonBlank([
roomProfile?.roomCover,
_payloadAssetText(payload['roomCoverUrl'] ?? payload['room_cover_url']),
_payloadAssetText(payload['roomCover'] ?? payload['room_cover']),
_payloadAssetText(payload['roomAvatar'] ?? payload['room_avatar']),
_payloadAssetText(payload['roomIcon'] ?? payload['room_icon']),
_payloadAssetText(payload['coverUrl'] ?? payload['cover_url']),
_payloadAssetText(payload['cover']),
_payloadAssetText(_broadcastPayloadMap(payload['room'])['coverUrl']),
_payloadAssetText(_broadcastPayloadMap(payload['room'])['cover']),
_payloadAssetText(_broadcastPayloadMap(payload['room'])['avatar']),
_payloadAssetText(
_broadcastPayloadMap(payload['roomInfo'])['coverUrl'],
),
_payloadAssetText(_broadcastPayloadMap(payload['roomInfo'])['cover']),
_payloadAssetText(_broadcastPayloadMap(payload['roomInfo'])['avatar']),
]),
roundNo:
_payloadNum(payload['roundNo'] ?? payload['round_no'])?.toInt() ?? 0,
level: level,
rocketIconUrl: _firstNonBlank([
_rocketIconFromPayload(payload),
_rocketIconUrlForLevel(level),
]),
rocketAnimationUrl: _rocketAnimationFromPayload(payload),
durationSeconds: _roomRocketPayloadNoticeSeconds(payload),
triggerUser: const RoomRocketLaunchTriggerUser(
userId: '',
nickname: '',
avatar: '',
account: '',
countryCode: '',
countryName: '',
),
triggerUserId: _payloadText(
payload['triggerUserId'] ?? payload['trigger_user_id'],
),
launchNo: _payloadText(payload['launchNo'] ?? payload['launch_no']),
);
}
double _roomRocketStatusPercent(dynamic status) {
if (status == null) {
return 0;
}
final value = status.displayPercent ?? status.energyPercent;
if (value is num) {
final raw = value.toDouble();
final percent = raw > 0 && raw <= 1 ? raw * 100 : raw;
return percent.clamp(0, 100).toDouble();
}
final currentEnergy = status.currentEnergy;
final maxEnergy = status.maxEnergy ?? status.needEnergy;
if (currentEnergy is! num || maxEnergy is! num || maxEnergy <= 0) {
return 0;
}
return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble();
}
double _roomRocketPayloadPercent(Map<String, dynamic> payload) {
final value = _payloadNum(
payload['displayPercent'] ??
payload['display_percent'] ??
payload['energyPercent'] ??
payload['energy_percent'] ??
payload['percent'],
);
if (value != null) {
final raw = value.toDouble();
final percent = raw > 0 && raw <= 1 ? raw * 100 : raw;
return percent.clamp(0, 100).toDouble();
}
final currentEnergy = _payloadNum(
payload['currentEnergy'] ?? payload['current_energy'],
);
final maxEnergy = _payloadNum(
payload['maxEnergy'] ??
payload['max_energy'] ??
payload['needEnergy'] ??
payload['need_energy'],
);
if (currentEnergy == null || maxEnergy == null || maxEnergy <= 0) {
return 0;
}
return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble();
}
int _roomRocketPayloadLevel(Map<String, dynamic> payload, int fallback) {
final value = _payloadNum(
payload['currentLevel'] ??
@ -2478,25 +2331,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
return (value?.toInt() ?? fallback).clamp(1, 99).toInt();
}
int _roomRocketPayloadNoticeSeconds(Map<String, dynamic> payload) {
final seconds =
_payloadNum(
payload['countdownSeconds'] ??
payload['countdown_seconds'] ??
payload['launchDelaySeconds'] ??
payload['launch_delay_seconds'] ??
payload['delaySeconds'] ??
payload['delay_seconds'] ??
payload['duration_seconds'] ??
payload['durationSeconds'],
)?.toInt() ??
10;
if (seconds > 0 && seconds <= 60) {
return seconds;
}
return 10;
}
String _rocketIconFromPayload(Map<String, dynamic> payload) {
return _firstNonBlank([
_payloadAssetText(payload['rocketIconUrl']),
@ -2508,22 +2342,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
]);
}
String _rocketAnimationFromPayload(Map<String, dynamic> payload) {
return _firstNonBlank([
_payloadAssetText(payload['rocketAnimationUrl']),
_payloadAssetText(payload['rocket_animation_url']),
_payloadAssetText(payload['rocketSvgaUrl']),
_payloadAssetText(payload['rocket_svga_url']),
_payloadAssetText(payload['animationUrl']),
_payloadAssetText(payload['animation_url']),
_payloadAssetText(payload['launchAnimationUrl']),
_payloadAssetText(payload['launch_animation_url']),
_payloadAssetText(payload['svgaUrl']),
_payloadAssetText(payload['sourceUrl']),
_payloadAssetText(payload['source_url']),
]);
}
String _payloadRoomId(Map<String, dynamic> payload) {
return _firstNonBlank([
_payloadText(payload['roomId']),

View File

@ -206,6 +206,9 @@ class TrtcRoomRtcEngineAdapter implements RoomRtcEngineAdapter {
@override
RoomRtcEngineKind get kind => RoomRtcEngineKind.trtc;
@override
bool get isInRoom => _isEntered;
@override
Future<void> prewarm() async {}

View File

@ -78,22 +78,28 @@ class SocialChatRoomManager extends ChangeNotifier {
void createNewRoom(BuildContext context, {String? customName}) async {
SCLoadingManager.show(context: context);
// 使
if (customName != null) {
// TODO:
}
await SCAccountRepository().createRoom();
myRoom = await SCAccountRepository().myProfile();
hasLoadedMyRoom = true;
isMyRoomLoading = false;
if (!context.mounted) {
try {
// 使
if (customName != null) {
// TODO:
}
await SCAccountRepository().createRoom();
myRoom = await SCAccountRepository().myProfile();
hasLoadedMyRoom = true;
isMyRoomLoading = false;
if (!context.mounted) {
return;
}
SCTts.show(SCAppLocalizations.of(context)!.createRoomSuccsess);
} catch (e) {
isMyRoomLoading = false;
if (context.mounted) {
SCTts.show(e.toString());
}
} finally {
notifyListeners();
SCLoadingManager.hide();
return;
}
SCTts.show(SCAppLocalizations.of(context)!.createRoomSuccsess);
notifyListeners();
SCLoadingManager.hide();
}
void clearContributionLevelData({bool notify = true}) {

View File

@ -848,6 +848,7 @@ class WearBadge {
String? milestone,
String? notSelectUrl,
String? selectUrl,
String? sourceType,
String? type,
String? userId,
bool? use,
@ -862,6 +863,7 @@ class WearBadge {
_milestone = milestone;
_notSelectUrl = notSelectUrl;
_selectUrl = selectUrl;
_sourceType = sourceType;
_type = type;
_userId = userId;
}
@ -882,7 +884,10 @@ class WearBadge {
_milestone = _stringValue(json['milestone']);
_notSelectUrl = _stringValue(json['notSelectUrl']);
_selectUrl = _stringValue(json['url'] ?? json['selectUrl']);
_type = _stringValue(json['type']);
final rawType = _stringValue(json['type']);
_sourceType =
_stringValue(json['sourceType']) ?? _sourceTypeFromType(rawType);
_type = rawType;
_userId = _stringValue(json['userId']);
}
@ -896,6 +901,7 @@ class WearBadge {
String? _milestone;
String? _notSelectUrl;
String? _selectUrl;
String? _sourceType;
String? _type;
String? _userId;
@ -910,6 +916,7 @@ class WearBadge {
String? milestone,
String? notSelectUrl,
String? selectUrl,
String? sourceType,
String? type,
String? userId,
}) => WearBadge(
@ -923,6 +930,7 @@ class WearBadge {
milestone: milestone ?? _milestone,
notSelectUrl: notSelectUrl ?? _notSelectUrl,
selectUrl: selectUrl ?? _selectUrl,
sourceType: sourceType ?? _sourceType,
type: type ?? _type,
userId: userId ?? _userId,
);
@ -947,6 +955,8 @@ class WearBadge {
String? get selectUrl => _selectUrl;
String? get sourceType => _sourceType;
String? get type => _type;
String? get userId => _userId;
@ -963,6 +973,7 @@ class WearBadge {
map['milestone'] = _milestone;
map['notSelectUrl'] = _notSelectUrl;
map['selectUrl'] = _selectUrl;
map['sourceType'] = _sourceType;
map['type'] = _type;
map['userId'] = _userId;
return map;
@ -973,6 +984,16 @@ class WearBadge {
return text == null || text.isEmpty ? null : text;
}
static String? _sourceTypeFromType(String? type) {
final normalized = type?.trim().toUpperCase();
if (normalized == "VIP" ||
normalized == "ACTIVITY" ||
normalized == "ACHIEVEMENT") {
return normalized;
}
return null;
}
static int? _intValue(dynamic value) {
if (value is int) {
return value;

View File

@ -71,7 +71,7 @@ class SCUserBadgeRes {
static bool _isDisplayBadgePayload(Map item) {
final type = _stringValue(item['type'])?.toUpperCase();
if (type != null && type != "LONG" && type != "SHORT") {
if (type != null && !_isDisplayType(type)) {
return false;
}
@ -84,6 +84,14 @@ class SCUserBadgeRes {
sourceType == "ACHIEVEMENT";
}
static bool _isDisplayType(String type) {
return type == "LONG" ||
type == "SHORT" ||
type == "VIP" ||
type == "ACTIVITY" ||
type == "ACHIEVEMENT";
}
static String? _stringValue(dynamic value) {
final text = value?.toString().trim();
return text == null || text.isEmpty ? null : text;

View File

@ -1174,10 +1174,10 @@ class SCAccountRepository implements SocialChatUserRepository {
return result;
}
///app/user/badge/current
///go/app/user/badge/current
@override
Future<SCUserBadgeRes> currentUserBadges() async {
const path = "/app/user/badge/current";
const path = "/go/app/user/badge/current";
final result = await http.get<SCUserBadgeRes>(
path,
extra: const {BaseNetworkClient.silentErrorToastKey: true},
@ -1186,11 +1186,11 @@ class SCAccountRepository implements SocialChatUserRepository {
return result;
}
///app/user/badge/other
///go/app/user/badge/other
@override
Future<SCUserBadgeRes> otherUserBadges(String userId) async {
final params = <String, dynamic>{"userId": userId};
const path = "/app/user/badge/other";
const path = "/go/app/user/badge/other";
final result = await http.get<SCUserBadgeRes>(
path,
queryParams: params,

View File

@ -37,7 +37,8 @@ class SCUserBadgeStrip extends StatefulWidget {
}
class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
static final Map<String, Future<SCUserBadgeRes>> _badgeFutures = {};
static const Duration _cacheTtl = Duration(minutes: 1);
static final Map<String, _BadgeFutureCacheEntry> _badgeFutures = {};
Future<SCUserBadgeRes>? _future;
@ -64,7 +65,16 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
final isCurrentUser = currentUserId.isNotEmpty && userId == currentUserId;
final cacheKey = isCurrentUser ? "current:$userId" : "other:$userId";
return _badgeFutures.putIfAbsent(cacheKey, () async {
final now = DateTime.now();
_badgeFutures.removeWhere(
(_, entry) => now.difference(entry.createdAt) >= _cacheTtl,
);
final cacheEntry = _badgeFutures[cacheKey];
if (cacheEntry != null &&
now.difference(cacheEntry.createdAt) < _cacheTtl) {
return cacheEntry.future;
}
final future = () async {
try {
return isCurrentUser
? await SCAccountRepository().currentUserBadges()
@ -73,7 +83,9 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
_badgeFutures.remove(cacheKey);
return SCUserBadgeRes.empty(userId: userId);
}
});
}();
_badgeFutures[cacheKey] = _BadgeFutureCacheEntry(future, now);
return future;
}
String get _normalizedUserId => widget.userId?.trim() ?? "";
@ -146,7 +158,8 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
Widget _buildBadge(WearBadge badge, {required double badgeHeight}) {
final type = badge.type?.trim().toUpperCase() ?? "";
final isLongBadge = type == "LONG";
final sourceType = badge.sourceType?.trim().toUpperCase() ?? "";
final isLongBadge = type == "LONG" || type == "VIP" || sourceType == "VIP";
final width =
isLongBadge
? (widget.longBadgeWidth ?? badgeHeight * 2.2)
@ -180,3 +193,10 @@ class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
return spaced;
}
}
class _BadgeFutureCacheEntry {
const _BadgeFutureCacheEntry(this.future, this.createdAt);
final Future<SCUserBadgeRes> future;
final DateTime createdAt;
}

View File

@ -1242,23 +1242,66 @@ class _MsgItemState extends State<MsgItem> {
);
}
Widget _buildUserNameAndLevels(SocialChatUserProfile? user) {
return SizedBox(
height: 23.w,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
fit: FlexFit.loose,
child: socialchatNickNameText(
user?.userNickname ?? "",
maxWidth: 112.w,
fontWeight: FontWeight.w500,
fontSize: 13.sp,
type: user?.getVIP()?.name ?? "",
needScroll: (user?.userNickname?.characters.length ?? 0) > 10,
),
),
SizedBox(width: 4.w),
getWealthLevel(
user?.wealthLevel ?? 0,
width: 42.w,
height: 20.w,
fontSize: 9.sp,
),
SizedBox(width: 2.w),
getUserLevel(
user?.charmLevel ?? 0,
width: 42.w,
height: 20.w,
fontSize: 9.sp,
),
],
),
);
}
List<WearBadge> _activeWearBadges(SocialChatUserProfile? user) {
return user?.wearBadge?.where((item) => item.use ?? false).toList() ??
const <WearBadge>[];
}
Widget _buildMedals({
required String? userId,
List<WearBadge> fallbackBadges = const <WearBadge>[],
}) {
final hasUserId = (userId ?? "").trim().isNotEmpty;
if (!hasUserId && fallbackBadges.isEmpty) {
return Container();
return SizedBox(height: 16.w);
}
return Expanded(
return SizedBox(
width: double.infinity,
child: SCUserBadgeStrip(
userId: userId,
fallbackBadges: fallbackBadges,
height: 25.w,
badgeHeight: 25.w,
longBadgeWidth: 55.w,
spacing: 5.w,
height: 16.w,
badgeHeight: 14.w,
longBadgeWidth: 34.w,
spacing: 3.w,
maxBadges: 8,
reserveSpace: true,
),
);
}
@ -1800,58 +1843,20 @@ class _MsgItemState extends State<MsgItem> {
),
SizedBox(width: 3.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
getWealthLevel(
widget.msg.user?.wealthLevel ?? 0,
width: 48.w,
height: 23.w,
fontSize: 10.sp,
),
getUserLevel(
widget.msg.user?.charmLevel ?? 0,
width: 48.w,
height: 23.w,
fontSize: 10.sp,
),
SizedBox(width: 3.w),
socialchatNickNameText(
maxWidth: 120.w,
fontWeight: FontWeight.w500,
widget.msg.user?.userNickname ?? "",
fontSize: 13.sp,
type: widget.msg.user?.getVIP()?.name ?? "",
needScroll:
(widget.msg.user?.userNickname?.characters.length ??
0) >
10,
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
widget.msg.user?.getVIP() != null
? netImage(
url: widget.msg.user?.getVIP()?.cover ?? "",
width: 25.w,
height: 25.w,
)
: Container(),
_buildMedals(
userId: widget.msg.user?.id,
fallbackBadges:
widget.msg.user?.wearBadge
?.where((item) => item.use ?? false)
.toList() ??
const <WearBadge>[],
),
],
),
],
child: SizedBox(
height: 48.w,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildUserNameAndLevels(widget.msg.user),
SizedBox(height: 2.w),
_buildMedals(
userId: widget.msg.user?.id,
fallbackBadges: _activeWearBadges(widget.msg.user),
),
],
),
),
),
],
@ -1884,56 +1889,20 @@ class _MsgItemState extends State<MsgItem> {
),
SizedBox(width: 3.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
getWealthLevel(
user.wealthLevel ?? 0,
width: 48.w,
height: 23.w,
fontSize: 10.sp,
),
getUserLevel(
user.charmLevel ?? 0,
width: 48.w,
height: 23.w,
fontSize: 10.sp,
),
SizedBox(width: 3.w),
socialchatNickNameText(
maxWidth: 120.w,
fontWeight: FontWeight.w500,
user.userNickname ?? "",
fontSize: 13.sp,
type: user.getVIP()?.name ?? "",
needScroll:
(user.userNickname?.characters.length ?? 0) > 10,
),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
user.getVIP() != null
? netImage(
url: user.getVIP()?.cover ?? "",
width: 25.w,
height: 25.w,
)
: Container(),
_buildMedals(
userId: user.id,
fallbackBadges:
user.wearBadge
?.where((item) => item.use ?? false)
.toList() ??
const <WearBadge>[],
),
],
),
],
child: SizedBox(
height: 48.w,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildUserNameAndLevels(user),
SizedBox(height: 2.w),
_buildMedals(
userId: user.id,
fallbackBadges: _activeWearBadges(user),
),
],
),
),
),
],

View File

@ -23,6 +23,8 @@ import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
import 'package:yumi/shared/tools/sc_room_utils.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart'
as room_card;
import 'package:yumi/services/general/sc_app_general_manager.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
@ -34,12 +36,12 @@ import '../../../shared/business_logic/models/res/sc_user_identity_res.dart';
import '../../../modules/chat/chat_route.dart';
class RoomUserInfoCard extends StatefulWidget {
String? userId;
final String? userId;
RoomUserInfoCard({super.key, this.userId});
const RoomUserInfoCard({super.key, this.userId});
@override
_RoomUserInfoCardState createState() => _RoomUserInfoCardState();
State<RoomUserInfoCard> createState() => _RoomUserInfoCardState();
}
class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
@ -180,7 +182,11 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
SizedBox(height: 6.w),
_buildInfoChips(context, profile),
SizedBox(height: 9.w),
_buildBadgeStrip(profile?.id ?? widget.userId),
_buildBadgeStrip(
profile?.id ?? widget.userId,
ref,
profile,
),
SizedBox(height: 10.w),
_buildActions(
context: context,
@ -408,9 +414,14 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
);
}
Widget _buildBadgeStrip(String? userId) {
Widget _buildBadgeStrip(
String? userId,
SocialChatUserProfileManager ref,
SocialChatUserProfile? profile,
) {
return SCUserBadgeStrip(
userId: userId,
fallbackBadges: _roomCardFallbackBadges(ref, profile),
height: 55.w,
badgeHeight: 23.w,
longBadgeWidth: 58.w,
@ -421,6 +432,46 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
);
}
List<WearBadge> _roomCardFallbackBadges(
SocialChatUserProfileManager ref,
SocialChatUserProfile? profile,
) {
final wearBadges =
profile?.wearBadge
?.where((item) => item.use != false && _hasBadgeUrl(item))
.toList() ??
const <WearBadge>[];
if (wearBadges.isNotEmpty) {
return wearBadges;
}
return ref.userCardInfo?.useBadge
?.map(_roomUseBadgeToWearBadge)
.where(_hasBadgeUrl)
.toList() ??
const <WearBadge>[];
}
WearBadge _roomUseBadgeToWearBadge(room_card.UseBadge badge) {
return WearBadge(
animationUrl: badge.animationUrl,
badgeKey: badge.badgeKey,
badgeLevel: badge.badgeLevel,
badgeName: badge.badgeName,
expireTime: badge.expireTime?.toInt(),
id: badge.id,
milestone: badge.milestone,
notSelectUrl: badge.notSelectUrl,
selectUrl: badge.selectUrl,
type: badge.type,
userId: badge.userId,
use: true,
);
}
bool _hasBadgeUrl(WearBadge badge) {
return (badge.selectUrl ?? "").trim().isNotEmpty;
}
Widget _buildActions({
required BuildContext context,
required SocialChatUserProfileManager ref,