静态礼物动画补充
This commit is contained in:
parent
afc6401e68
commit
ea99051267
@ -42,6 +42,7 @@ import 'services/shop/shop_manager.dart';
|
||||
import 'services/theme/theme_manager.dart';
|
||||
import 'services/auth/user_profile_manager.dart';
|
||||
import 'ui_kit/theme/socialchat_theme.dart';
|
||||
import 'ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart';
|
||||
import 'ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart';
|
||||
|
||||
bool _isCrashlyticsReady = false;
|
||||
@ -410,6 +411,16 @@ class _YumiApplicationState extends State<YumiApplication> {
|
||||
const Positioned.fill(
|
||||
child: VapPlusSvgaPlayer(tag: "room_gift"),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: RoomGiftSeatFlightOverlay(
|
||||
controller: RoomGiftSeatFlightController(),
|
||||
resolveTargetKey:
|
||||
(userId) => Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).getSeatGlobalKeyByIndex(userId),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@ -13,7 +13,9 @@ import 'package:yumi/shared/business_logic/models/res/join_room_res.dart';
|
||||
import 'package:yumi/services/gift/gift_animation_manager.dart';
|
||||
import 'package:yumi/services/gift/gift_system_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_screen.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/room_head_widget.dart';
|
||||
@ -42,6 +44,8 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()];
|
||||
final List<Widget> _tabs = [];
|
||||
late StreamSubscription _subscription;
|
||||
final RoomGiftSeatFlightController _giftSeatFlightController =
|
||||
RoomGiftSeatFlightController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -60,6 +64,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
context,
|
||||
listen: false,
|
||||
).toggleGiftAnimationVisibility(false);
|
||||
_giftSeatFlightController.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -70,6 +75,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
if (rtmProvider.msgFloatingGiftListener == _floatingGiftListener) {
|
||||
rtmProvider.msgFloatingGiftListener = null;
|
||||
}
|
||||
_giftSeatFlightController.clear();
|
||||
_tabController.dispose(); // 释放资源
|
||||
_subscription.cancel();
|
||||
super.dispose();
|
||||
@ -272,5 +278,80 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
context,
|
||||
listen: false,
|
||||
).enqueueGiftAnimation(giftModel);
|
||||
|
||||
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
|
||||
final targetUserId = _resolveGiftTargetUserId(msg);
|
||||
if (_shouldPlaySeatFlightGiftAnimation(msg) && targetUserId != null) {
|
||||
_giftSeatFlightController.enqueue(
|
||||
RoomGiftSeatFlightRequest(
|
||||
imagePath: giftPhoto,
|
||||
targetUserId: targetUserId,
|
||||
beginSize: 96.w,
|
||||
endSize: 28.w,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldPlaySeatFlightGiftAnimation(Msg msg) {
|
||||
final gift = msg.gift;
|
||||
if (gift == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final giftPhoto = (gift.giftPhoto ?? "").trim();
|
||||
if (giftPhoto.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final giftPhotoExt = _normalizedGiftResourceExtension(giftPhoto);
|
||||
if (_isAnimatedGiftResource(giftPhotoExt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final giftSourceUrl = (gift.giftSourceUrl ?? "").trim();
|
||||
final sourceExt = _normalizedGiftResourceExtension(giftSourceUrl);
|
||||
return !_isAnimatedGiftResource(sourceExt);
|
||||
}
|
||||
|
||||
bool _isAnimatedGiftResource(String extension) {
|
||||
return extension == ".svga" || extension == ".mp4" || extension == ".vap";
|
||||
}
|
||||
|
||||
String _normalizedGiftResourceExtension(String resource) {
|
||||
final value = resource.trim();
|
||||
if (value.isEmpty) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri != null && ((uri.scheme.isNotEmpty) || (uri.host.isNotEmpty))) {
|
||||
return SCPathUtils.getFileExtension(uri.path).toLowerCase();
|
||||
}
|
||||
|
||||
final normalizedValue = value.split("?").first.split("#").first;
|
||||
return SCPathUtils.getFileExtension(normalizedValue).toLowerCase();
|
||||
}
|
||||
|
||||
String? _resolveGiftTargetUserId(Msg msg) {
|
||||
final directUserId = (msg.toUser?.id ?? "").trim();
|
||||
if (directUserId.isNotEmpty) {
|
||||
return directUserId;
|
||||
}
|
||||
|
||||
final targetAccount = (msg.toUser?.account ?? "").trim();
|
||||
if (targetAccount.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
for (final micRes in rtcProvider.roomWheatMap.values) {
|
||||
if ((micRes.user?.account ?? "").trim() == targetAccount) {
|
||||
final userId = (micRes.user?.id ?? "").trim();
|
||||
if (userId.isNotEmpty) {
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
433
lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart
Normal file
433
lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart
Normal file
@ -0,0 +1,433 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
|
||||
typedef RoomGiftSeatTargetResolver =
|
||||
GlobalKey<State<StatefulWidget>>? Function(String targetUserId);
|
||||
|
||||
class RoomGiftSeatFlightRequest {
|
||||
const RoomGiftSeatFlightRequest({
|
||||
required this.imagePath,
|
||||
required this.targetUserId,
|
||||
this.holdDuration = const Duration(milliseconds: 780),
|
||||
this.flightDuration = const Duration(milliseconds: 920),
|
||||
this.beginSize = 96,
|
||||
this.endSize = 34,
|
||||
});
|
||||
|
||||
final String imagePath;
|
||||
final String targetUserId;
|
||||
final Duration holdDuration;
|
||||
final Duration flightDuration;
|
||||
final double beginSize;
|
||||
final double endSize;
|
||||
}
|
||||
|
||||
class RoomGiftSeatFlightController {
|
||||
static final RoomGiftSeatFlightController _instance =
|
||||
RoomGiftSeatFlightController._internal();
|
||||
|
||||
factory RoomGiftSeatFlightController() => _instance;
|
||||
|
||||
RoomGiftSeatFlightController._internal();
|
||||
|
||||
final Queue<RoomGiftSeatFlightRequest> _pendingRequests = Queue();
|
||||
_RoomGiftSeatFlightOverlayState? _state;
|
||||
|
||||
void enqueue(RoomGiftSeatFlightRequest request) {
|
||||
final imagePath = request.imagePath.trim();
|
||||
final targetUserId = request.targetUserId.trim();
|
||||
if (imagePath.isEmpty || targetUserId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final normalizedRequest = RoomGiftSeatFlightRequest(
|
||||
imagePath: imagePath,
|
||||
targetUserId: targetUserId,
|
||||
holdDuration: request.holdDuration,
|
||||
flightDuration: request.flightDuration,
|
||||
beginSize: request.beginSize,
|
||||
endSize: request.endSize,
|
||||
);
|
||||
|
||||
if (_state == null) {
|
||||
_pendingRequests.add(normalizedRequest);
|
||||
return;
|
||||
}
|
||||
_state!._enqueue(normalizedRequest);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_pendingRequests.clear();
|
||||
_state?._clear();
|
||||
}
|
||||
|
||||
void _attach(_RoomGiftSeatFlightOverlayState state) {
|
||||
_state = state;
|
||||
while (_pendingRequests.isNotEmpty) {
|
||||
state._enqueue(_pendingRequests.removeFirst());
|
||||
}
|
||||
}
|
||||
|
||||
void _detach(_RoomGiftSeatFlightOverlayState state) {
|
||||
if (_state == state) {
|
||||
_state = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RoomGiftSeatFlightOverlay extends StatefulWidget {
|
||||
const RoomGiftSeatFlightOverlay({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.resolveTargetKey,
|
||||
});
|
||||
|
||||
final RoomGiftSeatFlightController controller;
|
||||
final RoomGiftSeatTargetResolver resolveTargetKey;
|
||||
|
||||
@override
|
||||
State<RoomGiftSeatFlightOverlay> createState() =>
|
||||
_RoomGiftSeatFlightOverlayState();
|
||||
}
|
||||
|
||||
class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final Queue<_QueuedRoomGiftSeatFlightRequest> _queue = Queue();
|
||||
final GlobalKey _overlayKey = GlobalKey();
|
||||
|
||||
late final AnimationController _controller;
|
||||
|
||||
RoomGiftSeatFlightRequest? _activeRequest;
|
||||
ImageProvider<Object>? _activeImageProvider;
|
||||
Offset? _activeTargetOffset;
|
||||
bool _isPlaying = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this)..addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_finishCurrentAnimation();
|
||||
}
|
||||
});
|
||||
widget.controller._attach(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant RoomGiftSeatFlightOverlay oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller._detach(this);
|
||||
widget.controller._attach(this);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller._detach(this);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _enqueue(RoomGiftSeatFlightRequest request) {
|
||||
_queue.add(_QueuedRoomGiftSeatFlightRequest(request: request));
|
||||
_scheduleNextAnimation();
|
||||
}
|
||||
|
||||
void _clear() {
|
||||
_queue.clear();
|
||||
_controller.stop();
|
||||
_controller.reset();
|
||||
if (!mounted) {
|
||||
_activeRequest = null;
|
||||
_activeImageProvider = null;
|
||||
_activeTargetOffset = null;
|
||||
_isPlaying = false;
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_activeRequest = null;
|
||||
_activeImageProvider = null;
|
||||
_activeTargetOffset = null;
|
||||
_isPlaying = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _scheduleNextAnimation() {
|
||||
if (_isPlaying || _queue.isEmpty || !mounted) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_startNextAnimation();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _startNextAnimation() async {
|
||||
if (!mounted || _isPlaying || _queue.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final queuedRequest = _queue.removeFirst();
|
||||
final targetOffset = _resolveTargetOffset(
|
||||
queuedRequest.request.targetUserId,
|
||||
);
|
||||
if (targetOffset == null) {
|
||||
if (queuedRequest.retryCount < 6) {
|
||||
Future.delayed(const Duration(milliseconds: 120), () {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_queue.addFirst(
|
||||
queuedRequest.copyWith(retryCount: queuedRequest.retryCount + 1),
|
||||
);
|
||||
_scheduleNextAnimation();
|
||||
});
|
||||
} else {
|
||||
_scheduleNextAnimation();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final imageProvider = _resolveImageProvider(
|
||||
queuedRequest.request.imagePath,
|
||||
);
|
||||
_isPlaying = true;
|
||||
_controller.duration =
|
||||
queuedRequest.request.holdDuration +
|
||||
queuedRequest.request.flightDuration;
|
||||
|
||||
setState(() {
|
||||
_activeRequest = queuedRequest.request;
|
||||
_activeTargetOffset = targetOffset;
|
||||
_activeImageProvider = imageProvider;
|
||||
});
|
||||
|
||||
try {
|
||||
await precacheImage(
|
||||
imageProvider,
|
||||
context,
|
||||
).timeout(const Duration(milliseconds: 250));
|
||||
} catch (_) {}
|
||||
|
||||
if (!mounted || _activeRequest != queuedRequest.request) {
|
||||
return;
|
||||
}
|
||||
|
||||
_controller.reset();
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
void _finishCurrentAnimation() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_activeRequest = null;
|
||||
_activeImageProvider = null;
|
||||
_activeTargetOffset = null;
|
||||
_isPlaying = false;
|
||||
});
|
||||
_scheduleNextAnimation();
|
||||
}
|
||||
|
||||
Offset? _resolveTargetOffset(String targetUserId) {
|
||||
final overlayContext = _overlayKey.currentContext;
|
||||
final targetKey = widget.resolveTargetKey(targetUserId);
|
||||
final targetContext = targetKey?.currentContext;
|
||||
if (overlayContext == null || targetContext == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final overlayBox = overlayContext.findRenderObject();
|
||||
final targetBox = targetContext.findRenderObject();
|
||||
if (overlayBox is! RenderBox ||
|
||||
targetBox is! RenderBox ||
|
||||
!overlayBox.hasSize ||
|
||||
!targetBox.hasSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final globalTargetCenter = targetBox.localToGlobal(
|
||||
targetBox.size.center(Offset.zero),
|
||||
);
|
||||
return overlayBox.globalToLocal(globalTargetCenter);
|
||||
}
|
||||
|
||||
ImageProvider<Object> _resolveImageProvider(String imagePath) {
|
||||
if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
|
||||
return buildCachedImageProvider(imagePath);
|
||||
}
|
||||
if (imagePath.startsWith('/')) {
|
||||
return FileImage(File(imagePath));
|
||||
}
|
||||
return AssetImage(imagePath);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: SizedBox.expand(
|
||||
key: _overlayKey,
|
||||
child:
|
||||
_activeRequest == null ||
|
||||
_activeImageProvider == null ||
|
||||
_activeTargetOffset == null
|
||||
? const SizedBox.shrink()
|
||||
: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
final renderBox =
|
||||
_overlayKey.currentContext?.findRenderObject()
|
||||
as RenderBox?;
|
||||
final overlaySize =
|
||||
renderBox?.size ?? MediaQuery.sizeOf(context);
|
||||
final center = overlaySize.center(Offset.zero);
|
||||
final request = _activeRequest!;
|
||||
final totalDuration =
|
||||
request.holdDuration + request.flightDuration;
|
||||
final holdRatio =
|
||||
totalDuration.inMilliseconds == 0
|
||||
? 0.0
|
||||
: request.holdDuration.inMilliseconds /
|
||||
totalDuration.inMilliseconds;
|
||||
final progress = _controller.value;
|
||||
|
||||
if (progress <= holdRatio) {
|
||||
final normalizedHold =
|
||||
holdRatio == 0
|
||||
? 1.0
|
||||
: (progress / holdRatio).clamp(0.0, 1.0);
|
||||
final pulseScale =
|
||||
1 + math.sin(normalizedHold * math.pi) * 0.05;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
_buildGiftNode(
|
||||
center: center,
|
||||
size: request.beginSize,
|
||||
opacity: 1,
|
||||
scale: pulseScale,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final flightProgress = ((progress - holdRatio) /
|
||||
(1 - holdRatio))
|
||||
.clamp(0.0, 1.0);
|
||||
final travelProgress = Curves.easeInOutCubic.transform(
|
||||
flightProgress,
|
||||
);
|
||||
const trailGaps = <double>[0, 0.16, 0.32];
|
||||
const trailOpacities = <double>[1, 0.5, 0.24];
|
||||
const trailScales = <double>[1, 0.92, 0.84];
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children:
|
||||
List.generate(trailGaps.length, (index) {
|
||||
final trailingProgress = (travelProgress -
|
||||
trailGaps[index])
|
||||
.clamp(0.0, 1.0);
|
||||
final nodeCenter =
|
||||
Offset.lerp(
|
||||
center,
|
||||
_activeTargetOffset,
|
||||
trailingProgress,
|
||||
)!;
|
||||
final nodeSize =
|
||||
lerpDouble(
|
||||
request.beginSize,
|
||||
request.endSize,
|
||||
trailingProgress,
|
||||
)!;
|
||||
final fadeOut =
|
||||
1 -
|
||||
Curves.easeOut.transform(
|
||||
((travelProgress - 0.88) / 0.12).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
),
|
||||
);
|
||||
|
||||
return _buildGiftNode(
|
||||
center: nodeCenter,
|
||||
size: nodeSize,
|
||||
opacity: trailOpacities[index] * fadeOut,
|
||||
scale: trailScales[index],
|
||||
);
|
||||
}).reversed.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGiftNode({
|
||||
required Offset center,
|
||||
required double size,
|
||||
required double opacity,
|
||||
double scale = 1,
|
||||
}) {
|
||||
if (_activeImageProvider == null || opacity <= 0 || size <= 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
left: center.dx - size / 2,
|
||||
top: center.dy - size / 2,
|
||||
child: Opacity(
|
||||
opacity: opacity.clamp(0.0, 1.0),
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.22),
|
||||
blurRadius: 14,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Image(
|
||||
image: _activeImageProvider!,
|
||||
fit: BoxFit.contain,
|
||||
filterQuality: FilterQuality.low,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QueuedRoomGiftSeatFlightRequest {
|
||||
const _QueuedRoomGiftSeatFlightRequest({
|
||||
required this.request,
|
||||
this.retryCount = 0,
|
||||
});
|
||||
|
||||
final RoomGiftSeatFlightRequest request;
|
||||
final int retryCount;
|
||||
|
||||
_QueuedRoomGiftSeatFlightRequest copyWith({int? retryCount}) {
|
||||
return _QueuedRoomGiftSeatFlightRequest(
|
||||
request: request,
|
||||
retryCount: retryCount ?? this.retryCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
3
需求进度.md
3
需求进度.md
@ -13,6 +13,9 @@
|
||||
- 本轮按需求暂未处理网络链路上的启动等待,例如审核态检查或远端启动页配置请求。
|
||||
|
||||
## 已完成模块
|
||||
- 已将语言房送礼链路接入新的“中心停留后飞向目标麦位”组件,但只对无自带特效的静态 PNG 礼物生效:当前带自身 `SVGA/MP4/VAP` 动画或被识别为全屏礼物特效的礼物保持原有播放逻辑不变;只有普通 PNG 礼物会额外触发“屏幕中央停留 -> 三连残影飞向被赠送麦位”的补充动效,避免和自带礼物特效重复叠播。
|
||||
- 已继续收敛语言房送礼飞行动效的命中条件:上一版对普通礼物的过滤过严,既依赖 `giftPhoto` 必须显式以 `.png` 结尾,也会被部分礼物的 `special` 标记误伤,导致不少实际没有自带动画的礼物被提前跳过;当前已改为只排除真实带 `SVGA/MP4/VAP` 动画源的礼物,普通静态封面礼物即使是带 query 的图片 URL、或不是严格 `.png` 后缀,也会正常触发“中心停留 -> 飞向目标麦位”的补充动画。
|
||||
- 已将语言房送礼飞行动画从房间页内部 `Stack` 提升到应用根层,挂载方式对齐现有 `SVGA/VAP` 礼物特效层:当前该动画会和 `VapPlusSvgaPlayer` 一样在 `main.dart` 的顶层 builder 中全屏绘制,因此不会再被房间内部聊天区、局部动效或页面层级压住,视觉上更靠前、更容易被用户看到。
|
||||
- 已修复礼物页幸运礼物发送链路:当前 `gift_page` 会按礼物类型分流,普通礼物继续走 `/gift/batch`,`LUCKY_GIFT/LUCK/MAGIC` 改走已存在的 `/gift/give/lucky-gift`;幸运/魔法礼物成功后不再本地伪造普通 `GIFT` 消息,而是改发房间内 `LUCK_GIFT_ANIM_OTHER` 动画消息,避免和后端幸运礼物开奖/补发逻辑打架;同时送礼失败已补用户提示,且对 `standardId` 缺失的幸运礼物增加了前置拦截,避免继续点发送却只有日志没有反馈。
|
||||
- 已继续补齐幸运礼物前端可见反馈:根据真机日志确认 `giveLuckyGift` 和腾讯 IM 房间消息发送都已成功,当前问题收敛为发送端没有本地回显、且 `LUCK_GIFT_ANIM_OTHER` 收到后未真正接入房间礼物动画;现已在发送成功后先本地触发一轮房间礼物上飘反馈,并把收到的 `LUCK_GIFT_ANIM_OTHER` 直接接入现有房间礼物动画 listener,确保发送端和房间内其它端都能看到即时反应。排障期间临时加过的发送并发锁已在后续需求确认后撤回,不再限制幸运礼物连点。
|
||||
- 已按最新确认调整幸运礼物点击策略:撤回前一轮为排障临时加上的发送并发锁,恢复幸运礼物可连续点击;当前连点时每次请求仍走 `/gift/give/lucky-gift`,并继续通过本地回显 + `LUCK_GIFT_ANIM_OTHER` 接入现有房间礼物动画 listener 的方式提供与普通礼物一致的即时播报体验。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user