bug fix
This commit is contained in:
parent
35c05a9123
commit
01ba9d384b
@ -343,6 +343,7 @@
|
||||
"roomRedPacketHistoryTitle": "سجل إرسال حزم الحظ",
|
||||
"roomRedPacketHistoryItemTitle": "حزمة حظ ({1} مظروفاً)",
|
||||
"roomRedPacketHistoryStatusSent": "تم الإرسال",
|
||||
"roomRedPacketHistoryStatusFinished": "تم استلامها بالكامل",
|
||||
"roomRedPacketHistoryStatusPending": "قيد الانتظار",
|
||||
"roomRedPacketHistoryStatusReturned": "تم الإرجاع",
|
||||
"roomRedPacketHistoryRefund": "المسترد: {1}",
|
||||
|
||||
@ -191,6 +191,7 @@
|
||||
"roomRedPacketHistoryTitle": "লাকি প্যাক পাঠানোর ইতিহাস",
|
||||
"roomRedPacketHistoryItemTitle": "লাকি প্যাক ({1} খাম)",
|
||||
"roomRedPacketHistoryStatusSent": "পাঠানো হয়েছে",
|
||||
"roomRedPacketHistoryStatusFinished": "সব গ্রহণ করা হয়েছে",
|
||||
"roomRedPacketHistoryStatusPending": "অপেক্ষায়",
|
||||
"roomRedPacketHistoryStatusReturned": "ফেরত দেওয়া হয়েছে",
|
||||
"roomRedPacketHistoryRefund": "ফেরত: {1}",
|
||||
|
||||
@ -162,6 +162,7 @@
|
||||
"roomRedPacketHistoryTitle": "Lucky Pack Delivery History",
|
||||
"roomRedPacketHistoryItemTitle": "Lucky Bag ({1} red envelopes)",
|
||||
"roomRedPacketHistoryStatusSent": "Sent",
|
||||
"roomRedPacketHistoryStatusFinished": "Fully claimed",
|
||||
"roomRedPacketHistoryStatusPending": "Pending",
|
||||
"roomRedPacketHistoryStatusReturned": "Returned",
|
||||
"roomRedPacketHistoryRefund": "Refund: {1}",
|
||||
|
||||
@ -151,6 +151,7 @@
|
||||
"roomRedPacketHistoryTitle": "Şans Paketi Gönderim Geçmişi",
|
||||
"roomRedPacketHistoryItemTitle": "Şans Paketi ({1} zarf)",
|
||||
"roomRedPacketHistoryStatusSent": "Gönderildi",
|
||||
"roomRedPacketHistoryStatusFinished": "Tamamı alındı",
|
||||
"roomRedPacketHistoryStatusPending": "Beklemede",
|
||||
"roomRedPacketHistoryStatusReturned": "İade edildi",
|
||||
"roomRedPacketHistoryRefund": "İade: {1}",
|
||||
|
||||
@ -16,7 +16,7 @@ class SCVariant1Config implements AppConfig {
|
||||
@override
|
||||
String get apiHost => const String.fromEnvironment(
|
||||
'API_HOST',
|
||||
defaultValue: 'https://jvapi.haiyihy.com/',
|
||||
defaultValue: 'http://192.168.110.64:1100/',
|
||||
); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖 本地“http://192.168.110.64:1100/”,线上“https://jvapi.haiyihy.com/”
|
||||
|
||||
@override
|
||||
|
||||
@ -655,6 +655,9 @@ class SCAppLocalizations {
|
||||
String get roomRedPacketHistoryStatusSent =>
|
||||
translate('roomRedPacketHistoryStatusSent');
|
||||
|
||||
String get roomRedPacketHistoryStatusFinished =>
|
||||
translate('roomRedPacketHistoryStatusFinished');
|
||||
|
||||
String get roomRedPacketHistoryStatusPending =>
|
||||
translate('roomRedPacketHistoryStatusPending');
|
||||
|
||||
|
||||
@ -130,6 +130,29 @@ class VoiceRoomRoute implements SCIRouterProvider {
|
||||
});
|
||||
}
|
||||
|
||||
static void popVoiceRoomToPrevious(
|
||||
BuildContext context, {
|
||||
bool rootNavigator = false,
|
||||
}) {
|
||||
popVoiceRoomToPreviousOn(
|
||||
Navigator.of(context, rootNavigator: rootNavigator),
|
||||
);
|
||||
}
|
||||
|
||||
static void popVoiceRoomToPreviousOn(NavigatorState navigator) {
|
||||
if (!isVoiceRoomOpen) {
|
||||
return;
|
||||
}
|
||||
var removedVoiceRoom = false;
|
||||
navigator.popUntil((route) {
|
||||
if (route.settings.name == voiceRoom) {
|
||||
removedVoiceRoom = true;
|
||||
return false;
|
||||
}
|
||||
return removedVoiceRoom || route.isFirst;
|
||||
});
|
||||
}
|
||||
|
||||
static Future<T?> openRoomEdit<T>(
|
||||
BuildContext context, {
|
||||
required bool needRestCurrentRoomInfo,
|
||||
|
||||
@ -24,6 +24,7 @@ class _TaskPageState extends State<TaskPage>
|
||||
static const String _taskGameAsset = 'sc_images/index/sc_icon_task_game.png';
|
||||
static const String _taskGiftAsset = 'sc_images/index/sc_icon_task_gift.png';
|
||||
static const String _taskDefaultAsset = 'sc_images/index/sc_icon_task.png';
|
||||
static const Duration _taskStatusCacheRefreshDelay = Duration(seconds: 65);
|
||||
|
||||
final SCAccountRepository _repository = SCAccountRepository();
|
||||
final Set<String> _claimedTaskIds = {};
|
||||
@ -33,6 +34,7 @@ class _TaskPageState extends State<TaskPage>
|
||||
bool _loadingTasks = false;
|
||||
String? _toastMessage;
|
||||
Timer? _toastTimer;
|
||||
Timer? _cacheRefreshTimer;
|
||||
PageRoute<dynamic>? _routeObserverRoute;
|
||||
|
||||
@override
|
||||
@ -47,6 +49,7 @@ class _TaskPageState extends State<TaskPage>
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
routeObserver.unsubscribe(this);
|
||||
_toastTimer?.cancel();
|
||||
_cacheRefreshTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -68,6 +71,7 @@ class _TaskPageState extends State<TaskPage>
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
debugPrint('[TaskCenter][Page] refresh on app resumed');
|
||||
unawaited(_loadTasks());
|
||||
_scheduleTaskStatusCacheRefresh('app resumed');
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,6 +79,7 @@ class _TaskPageState extends State<TaskPage>
|
||||
void didPopNext() {
|
||||
debugPrint('[TaskCenter][Page] refresh on route return');
|
||||
unawaited(_loadTasks());
|
||||
_scheduleTaskStatusCacheRefresh('route return');
|
||||
}
|
||||
|
||||
Future<void> _loadTasks() async {
|
||||
@ -113,6 +118,27 @@ class _TaskPageState extends State<TaskPage>
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleManualRefresh() async {
|
||||
await _loadTasks();
|
||||
_scheduleTaskStatusCacheRefresh('manual refresh');
|
||||
}
|
||||
|
||||
void _scheduleTaskStatusCacheRefresh(String reason) {
|
||||
_cacheRefreshTimer?.cancel();
|
||||
debugPrint(
|
||||
'[TaskCenter][Page] schedule cache refresh reason=$reason '
|
||||
'delayMs=${_taskStatusCacheRefreshDelay.inMilliseconds}',
|
||||
);
|
||||
_cacheRefreshTimer = Timer(_taskStatusCacheRefreshDelay, () {
|
||||
_cacheRefreshTimer = null;
|
||||
if (!mounted) return;
|
||||
debugPrint(
|
||||
'[TaskCenter][Page] refresh after cache window reason=$reason',
|
||||
);
|
||||
unawaited(_loadTasks());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dailyTasks = _sectionTasks(isNewcomer: false);
|
||||
@ -131,7 +157,7 @@ class _TaskPageState extends State<TaskPage>
|
||||
child: RefreshIndicator(
|
||||
color: const Color(0xFF16DBB2),
|
||||
backgroundColor: const Color(0xFF06342A),
|
||||
onRefresh: _loadTasks,
|
||||
onRefresh: _handleManualRefresh,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(
|
||||
parent: BouncingScrollPhysics(),
|
||||
@ -355,6 +381,7 @@ class _TaskPageState extends State<TaskPage>
|
||||
if (opened && mounted) {
|
||||
debugPrint('[TaskCenter][Page] refresh after jump return');
|
||||
unawaited(_loadTasks());
|
||||
_scheduleTaskStatusCacheRefresh('jump return');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -21,8 +21,6 @@ import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/routes/sc_routes.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
@ -2653,6 +2651,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
type: SCRoomMsgType.systemTips,
|
||||
),
|
||||
);
|
||||
unawaited(
|
||||
rtmProvider?.loadRoomHistoryMessages(groupId, count: 20) ??
|
||||
Future<void>.value(),
|
||||
);
|
||||
|
||||
await Future.wait([microphoneListFuture, onlineUsersFuture]);
|
||||
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
|
||||
@ -2959,6 +2961,26 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return result;
|
||||
}
|
||||
|
||||
void sendRoomTextMessage(String message) {
|
||||
final normalizedMessage = message.trim();
|
||||
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
|
||||
if (normalizedMessage.isEmpty || groupId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
rtmProvider?.dispatchMessage(
|
||||
Msg(
|
||||
groupId: groupId,
|
||||
msg: normalizedMessage,
|
||||
type: SCRoomMsgType.text,
|
||||
role: currenRoom?.entrants?.roles ?? "",
|
||||
user: AccountStorage().getCurrentUser()?.userProfile,
|
||||
),
|
||||
) ??
|
||||
Future<void>.value(),
|
||||
);
|
||||
}
|
||||
|
||||
void _refreshRoomRedPacketListAfterEntry() {
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
|
||||
if (roomId.isEmpty) {
|
||||
@ -3065,10 +3087,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
_clearData();
|
||||
if (!isLogout && navigationContext != null) {
|
||||
SCNavigatorUtils.popUntil(
|
||||
navigationContext,
|
||||
ModalRoute.withName(SCRoutes.home),
|
||||
);
|
||||
VoiceRoomRoute.popVoiceRoomToPrevious(navigationContext);
|
||||
}
|
||||
|
||||
final agoraLeaveTask = _leaveAgoraRoomForExit();
|
||||
|
||||
@ -1029,6 +1029,83 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadRoomHistoryMessages(String groupId, {int count = 20}) async {
|
||||
final normalizedGroupId = groupId.trim();
|
||||
if (normalizedGroupId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final result = await TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.getGroupHistoryMessageList(
|
||||
groupID: normalizedGroupId,
|
||||
count: count < 10 ? 10 : count,
|
||||
lastMsgID: null,
|
||||
);
|
||||
final messages = result.data ?? const <V2TimMessage>[];
|
||||
debugPrint(
|
||||
'[RoomHistory][IM] loaded group=$normalizedGroupId count=${messages.length}',
|
||||
);
|
||||
for (final message in messages.reversed) {
|
||||
_addRoomHistoryMessage(normalizedGroupId, message);
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint(
|
||||
'[RoomHistory][IM] load failed group=$normalizedGroupId error=$error',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _addRoomHistoryMessage(String groupId, V2TimMessage message) {
|
||||
try {
|
||||
final customData = message.customElem?.data;
|
||||
if (customData == null || customData.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
final decoded = json.decode(customData);
|
||||
if (decoded is! Map) {
|
||||
return;
|
||||
}
|
||||
final data = decoded.map((key, value) => MapEntry(key.toString(), value));
|
||||
if (data['type'] == SCRoomMsgType.roomRedPacket) {
|
||||
final packetData = _broadcastPayloadMap(data['data']);
|
||||
if (packetData.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (!_isSameRoomRedPacketRegion(packetData)) {
|
||||
return;
|
||||
}
|
||||
RoomRedPacketPendingCache.instance.upsertPayload(packetData);
|
||||
addMsg(_buildRoomRedPacketMsg(groupId, packetData));
|
||||
return;
|
||||
}
|
||||
|
||||
final msg = Msg.fromJson(data);
|
||||
if ((msg.groupId ?? '').trim().isEmpty) {
|
||||
msg.groupId = groupId;
|
||||
}
|
||||
if (_shouldAddRoomHistoryMessage(msg)) {
|
||||
addMsg(msg);
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint(
|
||||
'[RoomHistory][IM] skip malformed msg=${message.msgID ?? ""} error=$error',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldAddRoomHistoryMessage(Msg msg) {
|
||||
switch (msg.type) {
|
||||
case SCRoomMsgType.text:
|
||||
case SCRoomMsgType.image:
|
||||
return true;
|
||||
case SCRoomMsgType.emoticons:
|
||||
return (msg.number ?? -1) < 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
addMsg(Msg msg) {
|
||||
final mergedGiftMsg = _mergeGiftMessageIfNeeded(msg);
|
||||
if (mergedGiftMsg != null) {
|
||||
@ -1572,19 +1649,20 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
bool _isCurrentVoiceRoom(String roomId) {
|
||||
final currentRoomId = _currentVoiceRoomId();
|
||||
return currentRoomId.isNotEmpty && currentRoomId == roomId.trim();
|
||||
}
|
||||
|
||||
String _currentVoiceRoomId() {
|
||||
final currentContext = context;
|
||||
if (roomId.trim().isEmpty ||
|
||||
currentContext == null ||
|
||||
!currentContext.mounted) {
|
||||
return false;
|
||||
if (currentContext == null || !currentContext.mounted) {
|
||||
return '';
|
||||
}
|
||||
final currentRoomId =
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
return Provider.of<RealTimeCommunicationManager>(
|
||||
currentContext,
|
||||
listen: false,
|
||||
).currenRoom?.roomProfile?.roomProfile?.id?.trim() ??
|
||||
'';
|
||||
return currentRoomId.isNotEmpty && currentRoomId == roomId.trim();
|
||||
}
|
||||
|
||||
Map<String, dynamic> _broadcastPayloadMap(dynamic data) {
|
||||
@ -1655,12 +1733,15 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
if (currentContext == null || !currentContext.mounted) {
|
||||
return;
|
||||
}
|
||||
final payloadMap = _broadcastPayloadMap(payload);
|
||||
final payloadRoomId = _payloadText(payloadMap['roomId']);
|
||||
if (payloadRoomId.isNotEmpty && !_isCurrentVoiceRoom(payloadRoomId)) {
|
||||
return;
|
||||
}
|
||||
if (_isCurrentUserRoomRedPacketPayload(payload)) {
|
||||
return;
|
||||
}
|
||||
final dialogData = RoomRedPacketUiData.fromJson(
|
||||
_broadcastPayloadMap(payload),
|
||||
);
|
||||
final dialogData = RoomRedPacketUiData.fromJson(payloadMap);
|
||||
if (!dialogData.hasPacketId ||
|
||||
!dialogData.isOpenable ||
|
||||
RoomRedPacketOpenDialog.isClaimedLocally(dialogData.packetId)) {
|
||||
@ -1811,11 +1892,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
: '',
|
||||
priority: 1000,
|
||||
);
|
||||
if (msg.roomId ==
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).currenRoom?.roomProfile?.roomProfile?.id) {
|
||||
final packetRoomId = _payloadText(fData["roomId"]);
|
||||
if (packetRoomId.isNotEmpty &&
|
||||
packetRoomId == _currentVoiceRoomId()) {
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
@ -1894,6 +1973,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
number: fData["totalCount"],
|
||||
priority: 1000,
|
||||
);
|
||||
final packetRoomId = _payloadText(fData["roomId"]);
|
||||
if (packetRoomId.isNotEmpty &&
|
||||
packetRoomId != _currentVoiceRoomId()) {
|
||||
return;
|
||||
}
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
|
||||
@ -13,8 +13,8 @@ class SCEntryPopupRes {
|
||||
final map = json is Map<String, dynamic> ? json : const <String, dynamic>{};
|
||||
final queue =
|
||||
(map['queue'] as List<dynamic>? ?? const <dynamic>[])
|
||||
.map((item) => item.toString())
|
||||
.where((item) => item.trim().isNotEmpty)
|
||||
.map((item) => item.toString().trim())
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toList();
|
||||
final popups = <String, SCEntryPopupItem>{};
|
||||
for (final entry in map.entries) {
|
||||
|
||||
@ -560,6 +560,9 @@ class OverlayManager {
|
||||
// 区域红包在当前房间仍然需要展示;区域礼物当前房间由 RTM 层跳过,避免和房间 IM 礼物飘屏重复。
|
||||
return message.type == 4;
|
||||
}
|
||||
if (message.type == 4 && rtcProvider.shouldShowRoomVisualEffects) {
|
||||
return true;
|
||||
}
|
||||
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -38,34 +38,57 @@ class SCEntryPopupCoordinator {
|
||||
BuildContext context, {
|
||||
SCEntryPopupRootTabSelector? onSelectRootTab,
|
||||
}) async {
|
||||
if (_isRunning || !context.mounted || AccountStorage().getToken().isEmpty) {
|
||||
if (_isRunning) {
|
||||
_log('skip reason=running');
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) {
|
||||
_log('skip reason=context-unmounted');
|
||||
return;
|
||||
}
|
||||
if (AccountStorage().getToken().isEmpty) {
|
||||
_log('skip reason=token-empty');
|
||||
return;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final lastRequestAt = _lastRequestAt;
|
||||
if (lastRequestAt != null &&
|
||||
now.difference(lastRequestAt) < _requestMinGap) {
|
||||
_log('skip reason=throttled');
|
||||
return;
|
||||
}
|
||||
_isRunning = true;
|
||||
_lastRequestAt = now;
|
||||
try {
|
||||
_log('request start');
|
||||
final response = await SCConfigRepositoryImp().entryPopup();
|
||||
_log(
|
||||
'response requestId=${response.requestId} '
|
||||
'queue=${response.queue} popups=${response.popups.keys.toList()}',
|
||||
);
|
||||
final popup = _pickDisplayablePopup(response);
|
||||
if (popup == null) {
|
||||
_log('skip reason=no-displayable-popup');
|
||||
return;
|
||||
}
|
||||
final imageUrl = normalizeImageResourceUrl(popup.image);
|
||||
final imageReady = await warmImageResource(
|
||||
popup.image,
|
||||
imageUrl,
|
||||
logicalWidth: 326,
|
||||
timeout: const Duration(seconds: 8),
|
||||
);
|
||||
if (!imageReady || !context.mounted) {
|
||||
_log(
|
||||
'skip reason=image-not-ready key=${popup.key} '
|
||||
'image=${_clip(imageUrl)} mounted=${context.mounted}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_log('show key=${popup.key} popupId=${popup.popupId}');
|
||||
await SCEntryPopupDialog.show(
|
||||
context,
|
||||
popup: popup,
|
||||
imageUrl: imageUrl,
|
||||
onDisplayed: () {
|
||||
unawaited(_markShown(popup));
|
||||
},
|
||||
@ -81,29 +104,64 @@ class SCEntryPopupCoordinator {
|
||||
}
|
||||
|
||||
static SCEntryPopupItem? _pickDisplayablePopup(SCEntryPopupRes response) {
|
||||
final queuedKeys = <String>{};
|
||||
for (final popupKey in response.queue) {
|
||||
queuedKeys.add(popupKey);
|
||||
final popup = response.popups[popupKey];
|
||||
if (popup == null || !_canShow(popup)) {
|
||||
if (popup == null) {
|
||||
_log('skip candidate key=$popupKey reason=missing-popup');
|
||||
continue;
|
||||
}
|
||||
final reason = _cannotShowReason(popup);
|
||||
if (reason != null) {
|
||||
_log('skip candidate key=${popup.key} reason=$reason');
|
||||
continue;
|
||||
}
|
||||
_log('candidate key=${popup.key} source=queue');
|
||||
return popup;
|
||||
}
|
||||
|
||||
final fallbackPopups =
|
||||
response.popups.values
|
||||
.where((popup) => !queuedKeys.contains(popup.key))
|
||||
.toList()
|
||||
..sort((left, right) => right.priority.compareTo(left.priority));
|
||||
for (final popup in fallbackPopups) {
|
||||
final reason = _cannotShowReason(popup);
|
||||
if (reason != null) {
|
||||
_log('skip fallback key=${popup.key} reason=$reason');
|
||||
continue;
|
||||
}
|
||||
_log('candidate key=${popup.key} source=priority-fallback');
|
||||
return popup;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool _canShow(SCEntryPopupItem popup) {
|
||||
if (!popup.value || popup.image.trim().isEmpty) {
|
||||
return false;
|
||||
static String? _cannotShowReason(SCEntryPopupItem popup) {
|
||||
if (!popup.value) {
|
||||
return 'disabled${popup.reason.isEmpty ? '' : '(${popup.reason})'}';
|
||||
}
|
||||
if (popup.image.trim().isEmpty) {
|
||||
return 'image-empty';
|
||||
}
|
||||
if (popup.limit <= 0) {
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
final lastShownAt = DataPersistence.getInt(_shownStorageKey(popup));
|
||||
final storageKey = _shownStorageKey(popup);
|
||||
final lastShownAt = DataPersistence.getInt(storageKey);
|
||||
if (lastShownAt <= 0) {
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
return nowMs - lastShownAt >= Duration(days: popup.limit).inMilliseconds;
|
||||
final elapsedMs = nowMs - lastShownAt;
|
||||
final limitMs = Duration(days: popup.limit).inMilliseconds;
|
||||
if (elapsedMs >= limitMs) {
|
||||
return null;
|
||||
}
|
||||
final remainHours =
|
||||
((limitMs - elapsedMs) / Duration.millisecondsPerHour).ceil();
|
||||
return 'limited key=$storageKey remainHours=$remainHours';
|
||||
}
|
||||
|
||||
static Future<bool> _markShown(SCEntryPopupItem popup) {
|
||||
@ -412,4 +470,15 @@ class SCEntryPopupCoordinator {
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
static void _log(String message) {
|
||||
debugPrint('[EntryPopup] $message');
|
||||
}
|
||||
|
||||
static String _clip(String value, {int maxLength = 180}) {
|
||||
if (value.length <= maxLength) {
|
||||
return value;
|
||||
}
|
||||
return '${value.substring(0, maxLength)}...';
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,35 @@ Map<String, String>? buildNetworkImageHeaders(String url) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
String normalizeImageResourceUrl(String resource) {
|
||||
final trimmed = resource.trim();
|
||||
if (trimmed.startsWith('//')) {
|
||||
final apiScheme = Uri.tryParse(SCGlobalConfig.apiHost)?.scheme;
|
||||
final scheme = apiScheme == 'http' ? 'http' : 'https';
|
||||
return '$scheme:$trimmed';
|
||||
}
|
||||
if (_isServerRelativeImagePath(trimmed)) {
|
||||
final baseUri = Uri.tryParse(SCGlobalConfig.apiHost);
|
||||
if (baseUri != null && baseUri.hasScheme && baseUri.host.isNotEmpty) {
|
||||
final path = trimmed.startsWith('/') ? trimmed : '/$trimmed';
|
||||
return baseUri.resolve(path).toString();
|
||||
}
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
bool _isServerRelativeImagePath(String value) {
|
||||
final lowerValue = value.toLowerCase();
|
||||
return lowerValue.startsWith('/external/') ||
|
||||
lowerValue.startsWith('external/') ||
|
||||
lowerValue.startsWith('/go/') ||
|
||||
lowerValue.startsWith('go/') ||
|
||||
lowerValue.startsWith('/static/') ||
|
||||
lowerValue.startsWith('static/') ||
|
||||
lowerValue.startsWith('/upload') ||
|
||||
lowerValue.startsWith('upload');
|
||||
}
|
||||
|
||||
int? _resolveImageProviderCacheDimension(double? logicalSize) {
|
||||
if (logicalSize == null || !logicalSize.isFinite || logicalSize <= 0) {
|
||||
return null;
|
||||
@ -54,18 +83,19 @@ ImageProvider<Object> buildCachedImageProvider(
|
||||
double? logicalWidth,
|
||||
double? logicalHeight,
|
||||
}) {
|
||||
final normalizedUrl = normalizeImageResourceUrl(url);
|
||||
late final ImageProvider<Object> provider;
|
||||
if (url.startsWith("http")) {
|
||||
if (normalizedUrl.startsWith("http")) {
|
||||
provider = ExtendedNetworkImageProvider(
|
||||
url,
|
||||
headers: buildNetworkImageHeaders(url),
|
||||
normalizedUrl,
|
||||
headers: buildNetworkImageHeaders(normalizedUrl),
|
||||
cache: true,
|
||||
cacheMaxAge: const Duration(days: 7),
|
||||
);
|
||||
} else if (url.startsWith('/')) {
|
||||
provider = FileImage(File(url));
|
||||
} else if (normalizedUrl.startsWith('/')) {
|
||||
provider = FileImage(File(normalizedUrl));
|
||||
} else {
|
||||
provider = AssetImage(url);
|
||||
provider = AssetImage(normalizedUrl);
|
||||
}
|
||||
return ResizeImage.resizeIfNeeded(
|
||||
_resolveImageProviderCacheDimension(logicalWidth),
|
||||
|
||||
@ -11,6 +11,7 @@ class SCEntryPopupDialog extends StatefulWidget {
|
||||
super.key,
|
||||
required this.popup,
|
||||
required this.onTap,
|
||||
this.imageUrl,
|
||||
this.onDisplayed,
|
||||
});
|
||||
|
||||
@ -18,12 +19,14 @@ class SCEntryPopupDialog extends StatefulWidget {
|
||||
|
||||
final SCEntryPopupItem popup;
|
||||
final VoidCallback onTap;
|
||||
final String? imageUrl;
|
||||
final VoidCallback? onDisplayed;
|
||||
|
||||
static Future<void> show(
|
||||
BuildContext context, {
|
||||
required SCEntryPopupItem popup,
|
||||
required VoidCallback onTap,
|
||||
String? imageUrl,
|
||||
VoidCallback? onDisplayed,
|
||||
}) async {
|
||||
SmartDialog.dismiss(tag: dialogTag);
|
||||
@ -37,6 +40,7 @@ class SCEntryPopupDialog extends StatefulWidget {
|
||||
(_) => SCEntryPopupDialog(
|
||||
popup: popup,
|
||||
onTap: onTap,
|
||||
imageUrl: imageUrl,
|
||||
onDisplayed: onDisplayed,
|
||||
),
|
||||
);
|
||||
@ -65,7 +69,7 @@ class _SCEntryPopupDialogState extends State<SCEntryPopupDialog> {
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
final dialogWidth = math.min(screenWidth - 42.w, 326.w);
|
||||
final imageUrl = widget.popup.image.trim();
|
||||
final imageUrl = (widget.imageUrl ?? widget.popup.image).trim();
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
|
||||
@ -6,8 +6,8 @@ import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/routes/sc_routes.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/modules/room/voice_room_route.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/gift/gift_animation_manager.dart';
|
||||
import 'package:yumi/services/music/room_music_manager.dart';
|
||||
@ -86,10 +86,7 @@ class _ExitMinRoomPageState extends State<ExitMinRoomPage> {
|
||||
unawaited(context.read<RoomMusicManager>().stopForRoomExit());
|
||||
giftAnimationManager.cleanupAnimationResources();
|
||||
SCFloatIchart().show();
|
||||
SCNavigatorUtils.popUntil(
|
||||
context,
|
||||
ModalRoute.withName(SCRoutes.home),
|
||||
);
|
||||
VoiceRoomRoute.popVoiceRoomToPrevious(context);
|
||||
},
|
||||
),
|
||||
SCDebounceWidget(
|
||||
|
||||
@ -313,13 +313,11 @@ class _FloatingRoomRedenvelopeScreenWidgetState
|
||||
}
|
||||
return;
|
||||
}
|
||||
SCRoomUtils.goRoom(
|
||||
roomId,
|
||||
currentContext,
|
||||
needOpenRedenvelope: true,
|
||||
redPackId: widget.message.toUserId ?? "",
|
||||
fromFloting: true,
|
||||
);
|
||||
if ((currentRoomId ?? '').trim().isEmpty) {
|
||||
rtcProvider.joinVoiceRoomSession(currentContext, roomId);
|
||||
return;
|
||||
}
|
||||
SCRoomUtils.goRoom(roomId, currentContext, fromFloting: true);
|
||||
}
|
||||
|
||||
String _senderNameForFloating() {
|
||||
|
||||
@ -247,8 +247,8 @@ class _RoomRedPacketHistoryTile extends StatelessWidget {
|
||||
switch (status) {
|
||||
case _RoomRedPacketHistoryStatus.sent:
|
||||
return localizations.roomRedPacketHistoryStatusSent;
|
||||
case _RoomRedPacketHistoryStatus.pending:
|
||||
return localizations.roomRedPacketHistoryStatusPending;
|
||||
case _RoomRedPacketHistoryStatus.finished:
|
||||
return localizations.roomRedPacketHistoryStatusFinished;
|
||||
case _RoomRedPacketHistoryStatus.returned:
|
||||
return localizations.roomRedPacketHistoryStatusReturned;
|
||||
}
|
||||
@ -258,8 +258,8 @@ class _RoomRedPacketHistoryTile extends StatelessWidget {
|
||||
switch (status) {
|
||||
case _RoomRedPacketHistoryStatus.sent:
|
||||
return const Color(0xFFFF4A3C);
|
||||
case _RoomRedPacketHistoryStatus.pending:
|
||||
return const Color(0xFFFFC857);
|
||||
case _RoomRedPacketHistoryStatus.finished:
|
||||
return const Color(0xFFFF4A3C);
|
||||
case _RoomRedPacketHistoryStatus.returned:
|
||||
return const Color(0xFF66D7A3);
|
||||
}
|
||||
@ -365,11 +365,19 @@ class _RoomRedPacketHistoryItem {
|
||||
SCRoomRedPacketSentRecordRes record,
|
||||
) {
|
||||
final status = record.status.toLowerCase();
|
||||
if (status.contains('return') || status.contains('refund')) {
|
||||
if (record.refundAmount > 0 ||
|
||||
status.contains('return') ||
|
||||
status.contains('refund')) {
|
||||
return _RoomRedPacketHistoryStatus.returned;
|
||||
}
|
||||
if (status.contains('pending') || status.contains('active')) {
|
||||
return _RoomRedPacketHistoryStatus.pending;
|
||||
final isFinishedByStatus =
|
||||
status.contains('finish') ||
|
||||
status.contains('complete') ||
|
||||
status.contains('sold') ||
|
||||
status.contains('empty');
|
||||
final isFinishedByCount = record.totalCount > 0 && record.remainCount <= 0;
|
||||
if (isFinishedByStatus || isFinishedByCount) {
|
||||
return _RoomRedPacketHistoryStatus.finished;
|
||||
}
|
||||
return _RoomRedPacketHistoryStatus.sent;
|
||||
}
|
||||
@ -399,4 +407,4 @@ String _formatHistoryTime(String raw) {
|
||||
|
||||
String _bidiSafeLtr(String value) => '\u200E$value\u200E';
|
||||
|
||||
enum _RoomRedPacketHistoryStatus { sent, pending, returned }
|
||||
enum _RoomRedPacketHistoryStatus { sent, finished, returned }
|
||||
|
||||
@ -24,6 +24,7 @@ class RoomRedPacketOpenDialog extends StatefulWidget {
|
||||
|
||||
static const String dialogTag = 'showRoomRedPacketOpenDialog';
|
||||
static final Map<String, num> _claimedAmountByPacketId = {};
|
||||
static final Set<String> _unavailablePacketIds = {};
|
||||
static final ValueNotifier<int> claimedStateNotifier = ValueNotifier<int>(0);
|
||||
|
||||
final RoomRedPacketUiData data;
|
||||
@ -85,6 +86,10 @@ class RoomRedPacketOpenDialog extends StatefulWidget {
|
||||
return _claimedAmountByPacketId.containsKey(_normalizePacketId(packetId));
|
||||
}
|
||||
|
||||
static bool isUnavailableLocally(String packetId) {
|
||||
return _unavailablePacketIds.contains(_normalizePacketId(packetId));
|
||||
}
|
||||
|
||||
static void rememberClaimed(String packetId, num amount) {
|
||||
final normalizedPacketId = _normalizePacketId(packetId);
|
||||
if (normalizedPacketId.isEmpty) {
|
||||
@ -95,11 +100,22 @@ class RoomRedPacketOpenDialog extends StatefulWidget {
|
||||
claimedStateNotifier.value++;
|
||||
}
|
||||
|
||||
static void rememberUnavailable(String packetId) {
|
||||
final normalizedPacketId = _normalizePacketId(packetId);
|
||||
if (normalizedPacketId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_unavailablePacketIds.add(normalizedPacketId);
|
||||
RoomRedPacketPendingCache.instance.removePacket(normalizedPacketId);
|
||||
claimedStateNotifier.value++;
|
||||
}
|
||||
|
||||
static void clearClaimedState() {
|
||||
if (_claimedAmountByPacketId.isEmpty) {
|
||||
if (_claimedAmountByPacketId.isEmpty && _unavailablePacketIds.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_claimedAmountByPacketId.clear();
|
||||
_unavailablePacketIds.clear();
|
||||
claimedStateNotifier.value++;
|
||||
}
|
||||
|
||||
@ -154,12 +170,7 @@ class RoomRedPacketOpenDialog extends StatefulWidget {
|
||||
static String _normalizePacketId(String packetId) => packetId.trim();
|
||||
|
||||
static bool _shouldIncludeRoomPacket(RoomRedPacketUiData data) {
|
||||
final normalizedStatus = data.statusDesc.toLowerCase();
|
||||
final explicitSoldOut =
|
||||
data.status == 2 ||
|
||||
normalizedStatus.contains('finish') ||
|
||||
normalizedStatus.contains('sold');
|
||||
return !data.grabbed && !data.isExpired && !explicitSoldOut;
|
||||
return !data.grabbed && !data.isExpired && !data.isSoldOut;
|
||||
}
|
||||
|
||||
static void _addPacketIfAbsent(
|
||||
@ -709,6 +720,7 @@ class _RoomRedPacketOpenCardState extends State<_RoomRedPacketOpenCard> {
|
||||
}
|
||||
final amount = result.amount ?? widget.data.mockClaimAmount;
|
||||
RoomRedPacketOpenDialog.rememberClaimed(widget.data.packetId, amount);
|
||||
_sendClaimSuccessRoomMessage(amount);
|
||||
setState(() {
|
||||
_claimAmount = amount;
|
||||
_state = _RoomRedPacketOpenState.opened;
|
||||
@ -719,6 +731,10 @@ class _RoomRedPacketOpenCardState extends State<_RoomRedPacketOpenCard> {
|
||||
return;
|
||||
}
|
||||
final code = roomRedPacketErrorCode(error);
|
||||
if (code == 'voice_room_red_packet_expired' ||
|
||||
code == 'voice_room_red_packet_finished') {
|
||||
RoomRedPacketOpenDialog.rememberUnavailable(widget.data.packetId);
|
||||
}
|
||||
setState(() {
|
||||
if (code == 'voice_room_red_packet_not_started') {
|
||||
_state = _RoomRedPacketOpenState.waiting;
|
||||
@ -755,6 +771,25 @@ class _RoomRedPacketOpenCardState extends State<_RoomRedPacketOpenCard> {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _sendClaimSuccessRoomMessage(num amount) {
|
||||
final localizations = SCAppLocalizations.of(context);
|
||||
if (localizations == null) {
|
||||
return;
|
||||
}
|
||||
final senderName =
|
||||
widget.data.senderName.trim().isNotEmpty
|
||||
? widget.data.senderName.trim()
|
||||
: localizations.redEnvelope;
|
||||
final message = localizations.roomRedPacketReceivedMessage(
|
||||
_formatNumber(amount.round()),
|
||||
senderName,
|
||||
);
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).sendRoomTextMessage(message);
|
||||
}
|
||||
|
||||
bool get _isLocalPreviewPacket {
|
||||
final packetId = widget.data.packetId.trim();
|
||||
return packetId.isEmpty ||
|
||||
|
||||
@ -82,6 +82,10 @@ class RoomRedPacketPendingCache extends ChangeNotifier {
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
void pruneExpired() {
|
||||
_pruneExpired();
|
||||
}
|
||||
|
||||
void removePacket(String packetId) {
|
||||
final normalizedPacketId = packetId.trim();
|
||||
if (normalizedPacketId.isEmpty) {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -51,9 +53,112 @@ class _RoomPlayWidgetState extends State<RoomPlayWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class _RoomRedPacketFloatingEntry extends StatelessWidget {
|
||||
class _RoomRedPacketFloatingEntry extends StatefulWidget {
|
||||
const _RoomRedPacketFloatingEntry();
|
||||
|
||||
@override
|
||||
State<_RoomRedPacketFloatingEntry> createState() =>
|
||||
_RoomRedPacketFloatingEntryState();
|
||||
}
|
||||
|
||||
class _RoomRedPacketFloatingEntryState
|
||||
extends State<_RoomRedPacketFloatingEntry> {
|
||||
static const int _listRefreshIntervalSeconds = 5;
|
||||
|
||||
Timer? _entryRefreshTimer;
|
||||
bool _refreshingList = false;
|
||||
int _elapsedSeconds = 0;
|
||||
String _verifiedRoomId = '';
|
||||
int _verifiedRoomRefreshCount = 0;
|
||||
Set<String> _verifiedKnownPacketIds = const <String>{};
|
||||
Set<String> _verifiedVisiblePacketIds = const <String>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_entryRefreshTimer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(_) => _onEntryTimerTick(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_entryRefreshTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onEntryTimerTick() {
|
||||
RoomRedPacketPendingCache.instance.pruneExpired();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
_elapsedSeconds++;
|
||||
if (_elapsedSeconds % _listRefreshIntervalSeconds == 0) {
|
||||
_refreshRoomRedPacketList();
|
||||
}
|
||||
}
|
||||
|
||||
void _refreshRoomRedPacketList() {
|
||||
if (_refreshingList || !mounted) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final currentRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
|
||||
if (currentRoomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_refreshingList = true;
|
||||
unawaited(
|
||||
rtcProvider
|
||||
.loadRoomRedPacketList(1)
|
||||
.then((result) {
|
||||
final serverPackets =
|
||||
result.map(RoomRedPacketUiData.fromListRes).toList();
|
||||
final serverPacketIds =
|
||||
serverPackets
|
||||
.map((item) => item.packetId.trim())
|
||||
.where((packetId) => packetId.isNotEmpty)
|
||||
.toSet();
|
||||
final visiblePacketIds =
|
||||
serverPackets
|
||||
.map(RoomRedPacketOpenDialog.withLocalClaimState)
|
||||
.where(_shouldShowRoomRedPacketEntry)
|
||||
.map((item) => item.packetId.trim())
|
||||
.where((packetId) => packetId.isNotEmpty)
|
||||
.toSet();
|
||||
if (!mounted) {
|
||||
return result;
|
||||
}
|
||||
setState(() {
|
||||
if (_verifiedRoomId == currentRoomId) {
|
||||
_verifiedRoomRefreshCount++;
|
||||
_verifiedKnownPacketIds = {
|
||||
..._verifiedKnownPacketIds,
|
||||
...serverPacketIds,
|
||||
};
|
||||
} else {
|
||||
_verifiedRoomId = currentRoomId;
|
||||
_verifiedRoomRefreshCount = 1;
|
||||
_verifiedKnownPacketIds = serverPacketIds;
|
||||
}
|
||||
_verifiedVisiblePacketIds = visiblePacketIds;
|
||||
});
|
||||
return result;
|
||||
})
|
||||
.catchError((_) => rtcProvider.redPacketList)
|
||||
.whenComplete(() {
|
||||
_refreshingList = false;
|
||||
}),
|
||||
);
|
||||
} catch (_) {
|
||||
_refreshingList = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
@ -67,6 +172,10 @@ class _RoomRedPacketFloatingEntry extends StatelessWidget {
|
||||
final snapshot = _RoomRedPacketEntrySnapshot.fromProviders(
|
||||
rtcProvider: rtcProvider,
|
||||
rtmProvider: rtmProvider,
|
||||
verifiedRoomId: _verifiedRoomId,
|
||||
verifiedRoomRefreshCount: _verifiedRoomRefreshCount,
|
||||
verifiedKnownPacketIds: _verifiedKnownPacketIds,
|
||||
verifiedVisiblePacketIds: _verifiedVisiblePacketIds,
|
||||
);
|
||||
if (snapshot.packets.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
@ -76,6 +185,10 @@ class _RoomRedPacketFloatingEntry extends StatelessWidget {
|
||||
final freshSnapshot = _RoomRedPacketEntrySnapshot.fromProviders(
|
||||
rtcProvider: Provider.of<RtcProvider>(context, listen: false),
|
||||
rtmProvider: Provider.of<RtmProvider>(context, listen: false),
|
||||
verifiedRoomId: _verifiedRoomId,
|
||||
verifiedRoomRefreshCount: _verifiedRoomRefreshCount,
|
||||
verifiedKnownPacketIds: _verifiedKnownPacketIds,
|
||||
verifiedVisiblePacketIds: _verifiedVisiblePacketIds,
|
||||
);
|
||||
if (freshSnapshot.packets.isEmpty) {
|
||||
return;
|
||||
@ -145,19 +258,41 @@ class _RoomRedPacketEntrySnapshot {
|
||||
factory _RoomRedPacketEntrySnapshot.fromProviders({
|
||||
required RtcProvider rtcProvider,
|
||||
required RtmProvider rtmProvider,
|
||||
required String verifiedRoomId,
|
||||
required int verifiedRoomRefreshCount,
|
||||
required Set<String> verifiedKnownPacketIds,
|
||||
required Set<String> verifiedVisiblePacketIds,
|
||||
}) {
|
||||
final packets = <RoomRedPacketUiData>[];
|
||||
final hiddenPacketIds = <String>{};
|
||||
final currentRoomId =
|
||||
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
|
||||
final shouldTrustVerifiedAbsence =
|
||||
currentRoomId.isNotEmpty &&
|
||||
verifiedRoomId == currentRoomId &&
|
||||
verifiedRoomRefreshCount >= 2;
|
||||
for (final packet in rtcProvider.redPacketList.map(
|
||||
RoomRedPacketUiData.fromListRes,
|
||||
)) {
|
||||
_addVisiblePacket(packets, packet);
|
||||
_addVisiblePacket(
|
||||
packets,
|
||||
packet,
|
||||
hiddenPacketIds: hiddenPacketIds,
|
||||
authoritative: true,
|
||||
);
|
||||
}
|
||||
for (final packet in RoomRedPacketPendingCache.instance.packetsForRoom(
|
||||
currentRoomId,
|
||||
)) {
|
||||
_addVisiblePacket(packets, packet);
|
||||
_addVisiblePacket(
|
||||
packets,
|
||||
packet,
|
||||
hiddenPacketIds: hiddenPacketIds,
|
||||
verifiedKnownPacketIds:
|
||||
shouldTrustVerifiedAbsence ? verifiedKnownPacketIds : null,
|
||||
verifiedVisiblePacketIds:
|
||||
shouldTrustVerifiedAbsence ? verifiedVisiblePacketIds : null,
|
||||
);
|
||||
}
|
||||
for (final msg in rtmProvider.roomChatMsgList) {
|
||||
if (msg.type != SCRoomMsgType.roomRedPacket) {
|
||||
@ -169,6 +304,11 @@ class _RoomRedPacketEntrySnapshot {
|
||||
payload: msg.msg,
|
||||
fallbackUser: msg.user,
|
||||
),
|
||||
hiddenPacketIds: hiddenPacketIds,
|
||||
verifiedKnownPacketIds:
|
||||
shouldTrustVerifiedAbsence ? verifiedKnownPacketIds : null,
|
||||
verifiedVisiblePacketIds:
|
||||
shouldTrustVerifiedAbsence ? verifiedVisiblePacketIds : null,
|
||||
);
|
||||
}
|
||||
final signature = packets
|
||||
@ -197,13 +337,40 @@ class _RoomRedPacketEntrySnapshot {
|
||||
|
||||
void _addVisiblePacket(
|
||||
List<RoomRedPacketUiData> packets,
|
||||
RoomRedPacketUiData packet,
|
||||
) {
|
||||
RoomRedPacketUiData packet, {
|
||||
Set<String>? hiddenPacketIds,
|
||||
Set<String>? verifiedKnownPacketIds,
|
||||
Set<String>? verifiedVisiblePacketIds,
|
||||
bool authoritative = false,
|
||||
}) {
|
||||
final normalizedPacket = RoomRedPacketOpenDialog.withLocalClaimState(packet);
|
||||
if (!_shouldShowRoomRedPacketEntry(normalizedPacket)) {
|
||||
final packetId = normalizedPacket.packetId.trim();
|
||||
if (packetId.isNotEmpty &&
|
||||
RoomRedPacketOpenDialog.isUnavailableLocally(packetId)) {
|
||||
if (authoritative) {
|
||||
hiddenPacketIds?.add(packetId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!authoritative &&
|
||||
packetId.isNotEmpty &&
|
||||
(hiddenPacketIds?.contains(packetId) ?? false)) {
|
||||
return;
|
||||
}
|
||||
if (!authoritative &&
|
||||
packetId.isNotEmpty &&
|
||||
verifiedKnownPacketIds != null &&
|
||||
verifiedVisiblePacketIds != null &&
|
||||
verifiedKnownPacketIds.contains(packetId) &&
|
||||
!verifiedVisiblePacketIds.contains(packetId)) {
|
||||
return;
|
||||
}
|
||||
if (!_shouldShowRoomRedPacketEntry(normalizedPacket)) {
|
||||
if (authoritative && packetId.isNotEmpty) {
|
||||
hiddenPacketIds?.add(packetId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final packetId = normalizedPacket.packetId.trim();
|
||||
if (packetId.isNotEmpty &&
|
||||
packets.any((item) => item.packetId.trim() == packetId)) {
|
||||
return;
|
||||
@ -212,10 +379,5 @@ void _addVisiblePacket(
|
||||
}
|
||||
|
||||
bool _shouldShowRoomRedPacketEntry(RoomRedPacketUiData item) {
|
||||
final normalizedStatus = item.statusDesc.toLowerCase();
|
||||
final explicitSoldOut =
|
||||
item.status == 2 ||
|
||||
normalizedStatus.contains('finish') ||
|
||||
normalizedStatus.contains('sold');
|
||||
return !item.grabbed && !item.isExpired && !explicitSoldOut;
|
||||
return !item.grabbed && !item.isExpired && !item.isSoldOut;
|
||||
}
|
||||
|
||||
3
需求进度.md
3
需求进度.md
@ -550,6 +550,9 @@
|
||||
- 已补齐红包领取记录页的发包摘要:从发红包记录页点击进入后,顶部固定展示该红包图标、红包数量、发送时间、发出金额和退款金额;即使领取列表为空仍展示红包金额,空态只作为领取人列表区域提示。
|
||||
- 已新增红包发送余额不足充值引导:发送接口返回 `voice_room_red_packet_insufficient_balance` 时关闭红包发送面板,改为弹出约半屏高度的底部充值弹窗,展示当前金币余额和充值按钮;点击充值复用 `me/wallet` 的 `WalletRoute.recharge` 进入充值页,返回后刷新用户余额。
|
||||
- 已按反馈统一红包入口图标:房间右侧待领红包入口去掉黑底和金色外圈,直接展示放大的红包图标,尺寸与游戏入口接近;房间菜单内红包项也从白色线性图标改为同一张红包图标。
|
||||
- 已继续修正红包跨房间与消息链路:同语区区域红包飘屏现在允许在其它语音房内展示;首页点击红包飘屏 `GO` 会直接进房,不再先弹确认或自动弹领取层;进房加入 IM 群后会拉取最近至少 10 条、当前按 20 条房间历史自定义消息并补入红包聊天记录;红包领取成功后,领取者会自动发送一条“从某用户 Lucky Pack 领取多少金币”的多语言房内 IM 文本消息。
|
||||
- 已继续修正红包入口状态刷新:房间右侧红包入口现在每秒清理过期缓存、每 5 秒刷新当前房间红包列表,并以后端列表里“已领/过期/抢完”的 packetId 作为权威隐藏来源;连续两次成功刷新后,若某红包曾在后端列表出现、但已不在后端可领列表中,也会过滤旧聊天消息或区域飘屏缓存,领取时遇到“已过期/已抢完”也会立刻本地隐藏入口,避免已不可领红包重新显示;同语区红包飘屏仍可跨房间展示,但即时红包领取弹层只会在 payload `roomId` 等于当前语音房时自动打开。
|
||||
- 已收口红包发送记录页状态展示:不再显示 `Pending/等待中`,后端 `ACTIVE/PENDING` 等中间态统一展示为已发出;只保留已退回、已领完、已发出三类状态,并补齐多语言的“已领完”文案。
|
||||
|
||||
## 已知问题
|
||||
- 已接入语音房表情包素材第一版:桌面 `表情素材` 中的 `fluent_emoji` 与 `monkey` gif 已纳入项目资源,底部聊天入口改为图稿里的短白色胶囊并移除内部输入图标,右侧新增独立表情按钮;同一个房间输入弹层现在可从表情按钮直接打开表情包面板,原 unicode emoji 面板已替换为本地 gif 表情。发送逻辑同步按最新规则分流:用户在麦上发表情时只在自己麦位播放,不进入聊天列表;用户未上麦发表情时进入房间聊天框展示,不触发麦位动画。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user