yumi-flutter/lib/modules/room/chat/widgets/room_chat_message_pane.dart
2026-07-08 09:53:03 +08:00

267 lines
7.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
import 'package:yumi/app/constants/sc_screen.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
enum RoomChatMessageChannel { all, chat, gift }
class RoomChatMessagePane extends StatefulWidget {
const RoomChatMessagePane({
super.key,
required this.channel,
this.onMessageTap,
});
final RoomChatMessageChannel channel;
final ValueChanged<SocialChatUserProfile?>? onMessageTap;
@override
State<RoomChatMessagePane> createState() => _RoomChatMessagePaneState();
}
class _RoomChatMessagePaneState extends State<RoomChatMessagePane> {
static const int _maxVisibleMessages = 250;
static const Duration _messageBatchWindow = Duration(milliseconds: 16);
final ScrollController _controller = ScrollController();
final List<Msg> _msgList = [];
final List<Msg> _pendingMessages = [];
Timer? _messageBatchTimer;
bool _isAtLatestMessage = true;
bool _isDisposed = false;
late RtmProvider _provider;
late final RoomNewMsgListener _newMsgListener = _onNewMsg;
@override
void initState() {
super.initState();
_controller.addListener(_scrollListener);
_provider = context.read<RtmProvider>();
_msgList.addAll(_trimmedInitialMessages(_provider, widget.channel));
_attachListener(_provider, widget.channel);
_jumpToLatestMessageAfterLayout();
}
@override
void didUpdateWidget(covariant RoomChatMessagePane oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.channel == widget.channel) {
return;
}
_detachListener(_provider, oldWidget.channel);
_messageBatchTimer?.cancel();
_messageBatchTimer = null;
_pendingMessages.clear();
_isAtLatestMessage = true;
_msgList
..clear()
..addAll(_trimmedInitialMessages(_provider, widget.channel));
_attachListener(_provider, widget.channel);
_jumpToLatestMessageAfterLayout();
}
@override
void dispose() {
_isDisposed = true;
_messageBatchTimer?.cancel();
_pendingMessages.clear();
_detachListener(_provider, widget.channel);
_controller.removeListener(_scrollListener);
_controller.dispose();
super.dispose();
}
List<Msg> _trimmedInitialMessages(
RtmProvider provider,
RoomChatMessageChannel channel,
) {
final messages = switch (channel) {
RoomChatMessageChannel.all => provider.roomAllMsgList,
RoomChatMessageChannel.chat => provider.roomChatMsgList,
RoomChatMessageChannel.gift => provider.roomGiftMsgList,
};
return messages.take(_maxVisibleMessages).toList(growable: false);
}
void _attachListener(RtmProvider provider, RoomChatMessageChannel channel) {
switch (channel) {
case RoomChatMessageChannel.all:
provider.msgAllListener = _newMsgListener;
return;
case RoomChatMessageChannel.chat:
provider.msgChatListener = _newMsgListener;
return;
case RoomChatMessageChannel.gift:
provider.msgGiftListener = _newMsgListener;
return;
}
}
void _detachListener(RtmProvider provider, RoomChatMessageChannel channel) {
switch (channel) {
case RoomChatMessageChannel.all:
if (identical(provider.msgAllListener, _newMsgListener)) {
provider.msgAllListener = null;
}
return;
case RoomChatMessageChannel.chat:
if (identical(provider.msgChatListener, _newMsgListener)) {
provider.msgChatListener = null;
}
return;
case RoomChatMessageChannel.gift:
if (identical(provider.msgGiftListener, _newMsgListener)) {
provider.msgGiftListener = null;
}
return;
}
}
void _scrollListener() {
final position = _controller.position;
const tolerance = 10.0;
final isAtLatest = position.pixels < position.minScrollExtent + tolerance;
if (isAtLatest != _isAtLatestMessage) {
_isAtLatestMessage = isAtLatest;
if (!_isDisposed) {
setState(() {});
}
}
}
void _onNewMsg(Msg msg) {
if (_isDisposed) {
return;
}
if (msg.groupId == "-1000") {
_clearMessages();
return;
}
_pendingMessages.add(msg);
_scheduleMessageBatchFlush();
}
void _clearMessages() {
_messageBatchTimer?.cancel();
_messageBatchTimer = null;
_pendingMessages.clear();
_isAtLatestMessage = true;
_msgList.clear();
setState(() {});
}
void _scheduleMessageBatchFlush() {
if (_messageBatchTimer?.isActive ?? false) {
return;
}
// 房间消息会成批到达,按一帧合并 setState避免每条消息都触发布局和滚动。
_messageBatchTimer = Timer(_messageBatchWindow, _flushPendingMessages);
}
void _flushPendingMessages() {
_messageBatchTimer = null;
if (_isDisposed || _pendingMessages.isEmpty) {
return;
}
final messages = List<Msg>.of(_pendingMessages);
_pendingMessages.clear();
final shouldStickToLatest = _isAtLatestMessage;
setState(() {
for (final msg in messages) {
_mergeMessage(msg);
}
if (_msgList.length > _maxVisibleMessages) {
_msgList.removeRange(_maxVisibleMessages, _msgList.length);
}
});
if (shouldStickToLatest) {
_jumpToLatestMessageAfterLayout();
}
}
void _mergeMessage(Msg msg) {
if (widget.channel == RoomChatMessageChannel.chat &&
_msgList.contains(msg)) {
return;
}
final existingIndex = _msgList.indexOf(msg);
if (existingIndex >= 0) {
_msgList.removeAt(existingIndex);
}
_msgList.insert(0, msg);
}
void _jumpToLatestMessageAfterLayout() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_isDisposed) {
return;
}
_jumpToLatestMessage();
});
}
void _jumpToLatestMessage() {
if (!_controller.hasClients) {
return;
}
_controller.jumpTo(_controller.position.minScrollExtent);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
resizeToAvoidBottomInset: false,
body: Stack(
alignment: Alignment.bottomLeft,
children: [
ListView.builder(
reverse: true,
controller: _controller,
// 父级已经提供确定高度;不使用 shrinkWrap避免每批消息重算全部 item 高度。
itemBuilder:
(BuildContext context, int index) => MsgItem(
msg: _msgList[index],
onClick: (user) => widget.onMessageTap?.call(user),
),
itemCount: _msgList.length,
),
Visibility(
visible: !_isAtLatestMessage,
child: GestureDetector(
onTap: _jumpToLatestMessage,
child: Container(
margin: EdgeInsets.symmetric(horizontal: width(15)),
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(52),
color: Colors.white,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
SCAppLocalizations.of(context)!.scrollToTheBottom,
style: TextStyle(fontSize: 10.sp),
),
SizedBox(width: 4.w),
Icon(Icons.chevron_right, size: 10.w),
],
),
),
),
),
],
),
);
}
}