Browse Source

添加聊天室公屏发送消息,封装优化工具类

ios
Jolie 4 months ago
parent
commit
68a1597bae
11 changed files with 563 additions and 93 deletions
  1. 85
      lib/controller/discover/room_controller.dart
  2. 37
      lib/model/live/live_chat_message.dart
  3. 37
      lib/pages/discover/live_room_page.dart
  4. 11
      lib/pages/home/nearby_tab.dart
  5. 11
      lib/pages/home/recommend_tab.dart
  6. 47
      lib/rtc/rtm_manager.dart
  7. 250
      lib/service/live_chat_message_service.dart
  8. 32
      lib/widget/live/live_room_action_bar.dart
  9. 40
      lib/widget/live/live_room_chat_item.dart
  10. 79
      lib/widget/live/live_room_notice_chat_panel.dart
  11. 27
      lib/widget/live/live_room_user_header.dart

85
lib/controller/discover/room_controller.dart

@ -1,10 +1,10 @@
import 'package:agora_token_generator/agora_token_generator.dart';
import 'package:dating_touchme_app/model/live/live_chat_message.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart';
import 'package:dating_touchme_app/network/network_service.dart';
import 'package:dating_touchme_app/rtc/rtc_manager.dart';
import 'package:dating_touchme_app/service/live_chat_message_service.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:permission_handler/permission_handler.dart';
///
@ -20,6 +20,61 @@ class RoomController extends GetxController {
///
final RxBool isLoading = false.obs;
///
final RxList<LiveChatMessage> chatMessages = <LiveChatMessage>[].obs;
///
final LiveChatMessageService _messageService = LiveChatMessageService.instance;
@override
void onInit() {
super.onInit();
//
_registerMessageListener();
}
@override
void onClose() {
super.onClose();
//
_messageService.unregisterMessageListener();
}
///
void _registerMessageListener() {
_messageService.registerMessageListener(
onMessageReceived: (message) {
_addMessage(message);
},
onMessageError: (error) {
print('❌ 消息处理错误: $error');
},
);
}
///
void _addMessage(LiveChatMessage message) {
// userId + content + timestamp
final exists = chatMessages.any((m) =>
m.userId == message.userId &&
m.content == message.content &&
(m.timestamp - message.timestamp).abs() < 1000); // 1
if (exists) {
print('⚠️ 消息已存在,跳过添加');
return;
}
chatMessages.add(message);
print('✅ 消息已添加到列表,当前消息数: ${chatMessages.length}');
// 100
if (chatMessages.length > 100) {
chatMessages.removeAt(0);
print('📝 消息列表已满,移除最旧的消息');
}
}
/// RTC
Future<void> createRtcChannel() async {
if (isLoading.value) return;
@ -32,14 +87,6 @@ class RoomController extends GetxController {
final base = response.data;
if (base.isSuccess && base.data != null) {
rtcChannel.value = base.data;
GetStorage storage = GetStorage();
String userId = storage.read('userId') ?? '';
String tokens = RtmTokenBuilder.buildToken(
appId: '4c2ea9dcb4c5440593a418df0fdd512d',
appCertificate: '16f34b45181a4fae8acdb1a28762fcfa',
userId: userId,
tokenExpireSeconds: 3600,
);
await _joinRtcChannel(base.data!.token, base.data!.channelId, base.data!.uid);
} else {
final message = base.message.isNotEmpty ? base.message : '创建频道失败';
@ -67,8 +114,24 @@ class RoomController extends GetxController {
}
}
///
Future<void> sendChatMessage(String content) async {
final channelName = rtcChannel.value?.channelId ?? RTCManager.instance.currentChannelId;
final result = await _messageService.sendMessage(
content: content,
channelName: channelName,
);
// RTM
if (result.success && result.message != null) {
_addMessage(result.message!);
}
}
///
Future<void> sendMessage(String message) async {
await RTCManager.instance.sendMessage(message);
await sendChatMessage(message);
}
Future<bool> _ensureRtcPermissions() async {

37
lib/model/live/live_chat_message.dart

@ -0,0 +1,37 @@
///
class LiveChatMessage {
final String userId;
final String userName;
final String? avatar;
final String content;
final int timestamp;
LiveChatMessage({
required this.userId,
required this.userName,
this.avatar,
required this.content,
required this.timestamp,
});
factory LiveChatMessage.fromJson(Map<String, dynamic> json) {
return LiveChatMessage(
userId: json['userId'] ?? json['uid'] ?? '',
userName: json['userName'] ?? json['nickName'] ?? '用户',
avatar: json['avatar'] ?? json['profilePhoto'],
content: json['content'] ?? json['message'] ?? '',
timestamp: json['timestamp'] ?? DateTime.now().millisecondsSinceEpoch,
);
}
Map<String, dynamic> toJson() {
return {
'userId': userId,
'userName': userName,
'avatar': avatar,
'content': content,
'timestamp': timestamp,
};
}
}

37
lib/pages/discover/live_room_page.dart

@ -1,4 +1,5 @@
import 'package:dating_touchme_app/controller/discover/room_controller.dart';
import 'package:dating_touchme_app/controller/global.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@ -79,6 +80,21 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
super.dispose();
}
///
Future<void> _sendMessage() async {
final content = _messageController.text.trim();
if (content.isEmpty) {
return;
}
//
await _roomController.sendChatMessage(content);
//
_messageController.clear();
message = '';
}
void _showGiftPopup() {
SmartDialog.show(
alignment: Alignment.bottomCenter,
@ -143,9 +159,23 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
child: Column(
children: [
SizedBox(height: 10.w),
const LiveRoomUserHeader(
userName: '开心的橘子',
popularityText: '1263',
Builder(
builder: (context) {
// GlobalData
final userData = GlobalData().userData;
final userName = userData?.nickName ?? '用户';
//
final popularityText = '0'; // TODO:
return LiveRoomUserHeader(
userName: userName,
popularityText: popularityText,
avatarAsset: (userData?.profilePhoto != null &&
userData!.profilePhoto!.isNotEmpty)
? userData.profilePhoto!
: Assets.imagesUserAvatar,
);
},
),
SizedBox(height: 7.w),
LiveRoomAnchorShowcase(),
@ -165,6 +195,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
onMessageChanged: (value) {
message = value;
},
onSendTap: _sendMessage,
onGiftTap: _showGiftPopup,
onChargeTap: _showRechargePopup,
),

11
lib/pages/home/nearby_tab.dart

@ -40,7 +40,6 @@ class _NearbyTabState extends State<NearbyTab>
return Obx(() {
final List<MarriageData> dataSource = controller.nearbyFeed;
final bool isLoading = controller.nearbyIsLoading.value;
final bool hasMore = controller.nearbyHasMore.value;
return SmartRefresher(
@ -106,16 +105,8 @@ class _NearbyTabState extends State<NearbyTab>
bottom: totalBottomPadding + 12,
),
itemBuilder: (context, index) {
//
if (isLoading && dataSource.isEmpty && index == 0) {
// 使
return SizedBox(
height: MediaQuery.of(context).size.height * 1.2,
child: const Center(child: CircularProgressIndicator()),
);
}
//
if (!isLoading && dataSource.isEmpty && index == 0) {
if (dataSource.isEmpty && index == 0) {
// 使
return SizedBox(
height: MediaQuery.of(context).size.height * 1.2,

11
lib/pages/home/recommend_tab.dart

@ -40,7 +40,6 @@ class _RecommendTabState extends State<RecommendTab>
return Obx(() {
final List<MarriageData> dataSource = controller.recommendFeed;
final bool isLoading = controller.recommendIsLoading.value;
final bool hasMore = controller.recommendHasMore.value;
return SmartRefresher(
@ -106,16 +105,8 @@ class _RecommendTabState extends State<RecommendTab>
bottom: totalBottomPadding + 12,
),
itemBuilder: (context, index) {
//
if (isLoading && dataSource.isEmpty && index == 0) {
// 使
return SizedBox(
height: MediaQuery.of(context).size.height * 1.2,
child: const Center(child: CircularProgressIndicator()),
);
}
//
if (!isLoading && dataSource.isEmpty && index == 0) {
if (dataSource.isEmpty && index == 0) {
// 使
return SizedBox(
height: MediaQuery.of(context).size.height * 1.2,

47
lib/rtc/rtm_manager.dart

@ -4,9 +4,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../model/rtc/rtc_channel_data.dart';
import '../network/network_service.dart';
import '../network/rtc_api.dart';
/// RTM StreamChannel
class RTMManager {
@ -148,15 +146,42 @@ class RTMManager {
String? customType,
bool storeInHistory = false,
}) async {
_ensureInitialized();
final (status, _) = await _client!.publish(
channelName,
message,
channelType: channelType,
customType: customType,
storeInHistory: storeInHistory,
);
return _handleStatus(status);
try {
_ensureInitialized();
if (!_isLoggedIn) {
print('❌ RTM 未登录,无法发布消息');
return false;
}
print('📤 RTM 发布消息到频道: $channelName');
print('📤 RTM 消息内容: $message');
print('📤 RTM 开始调用 publish 方法...');
final (status, _) = await _client!.publish(
channelName,
message,
channelType: channelType,
customType: customType,
storeInHistory: storeInHistory,
);
print('📤 RTM publish 方法返回,状态码: ${status.errorCode}');
print('📤 RTM publish 错误状态: ${status.error}');
final success = _handleStatus(status);
if (success) {
print('✅ RTM 消息发布成功');
} else {
print('❌ RTM 消息发布失败: ${status.errorCode}');
if (onOperationError != null) {
onOperationError!(status);
}
}
return success;
} catch (e, stackTrace) {
print('❌ RTM publishChannelMessage 异常: $e');
print('❌ 堆栈信息: $stackTrace');
return false;
}
}
/// RTM Client

250
lib/service/live_chat_message_service.dart

@ -0,0 +1,250 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:agora_rtm/agora_rtm.dart';
import 'package:dating_touchme_app/controller/global.dart';
import 'package:dating_touchme_app/model/live/live_chat_message.dart';
import 'package:dating_touchme_app/rtc/rtc_manager.dart';
import 'package:dating_touchme_app/rtc/rtm_manager.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get_storage/get_storage.dart';
///
///
class LiveChatMessageService {
//
static final LiveChatMessageService _instance = LiveChatMessageService._internal();
factory LiveChatMessageService() => _instance;
static LiveChatMessageService get instance => _instance;
final GetStorage _storage = GetStorage();
bool _isListenerRegistered = false;
//
Function(LiveChatMessage message)? onMessageReceived;
Function(String error)? onMessageError;
LiveChatMessageService._internal();
///
void registerMessageListener({
Function(LiveChatMessage message)? onMessageReceived,
Function(String error)? onMessageError,
}) {
if (_isListenerRegistered) {
print('⚠️ 消息监听器已注册,跳过重复注册');
return;
}
this.onMessageReceived = onMessageReceived;
this.onMessageError = onMessageError;
RTMManager.instance.onMessageEvent = (MessageEvent event) {
_handleIncomingMessage(event);
};
_isListenerRegistered = true;
print('✅ 消息监听器注册完成');
}
///
void unregisterMessageListener() {
RTMManager.instance.onMessageEvent = null;
_isListenerRegistered = false;
onMessageReceived = null;
onMessageError = null;
print('✅ 消息监听器已移除');
}
///
void _handleIncomingMessage(MessageEvent event) {
try {
//
final messageText = _parseMessageContent(event.message);
final messageData = json.decode(messageText) as Map<String, dynamic>;
//
if (messageData['type'] == 'chat_message') {
final chatMessage = LiveChatMessage.fromJson(messageData);
onMessageReceived?.call(chatMessage);
}
} catch (e, stackTrace) {
final error = '解析RTM消息失败: $e';
print('$error');
print('❌ 堆栈信息: $stackTrace');
onMessageError?.call(error);
}
}
/// String Uint8List
String _parseMessageContent(dynamic message) {
if (message is String) {
return message;
} else if (message is Uint8List) {
return utf8.decode(message);
} else {
return message.toString();
}
}
///
Map<String, dynamic> _buildMessageData(String content) {
final userId = _storage.read('userId') ?? GlobalData().userId ?? '';
final userData = GlobalData().userData;
final userName = userData?.nickName ?? '用户';
final avatar = userData?.profilePhoto;
return {
'type': 'chat_message',
'userId': userId,
'userName': userName,
'avatar': avatar,
'content': content.trim(),
'timestamp': DateTime.now().millisecondsSinceEpoch,
};
}
///
String? _validateMessage(String content) {
final trimmed = content.trim();
if (trimmed.isEmpty) {
return '消息内容不能为空';
}
if (trimmed.length > 500) {
return '消息内容不能超过500个字符';
}
return null;
}
///
String? _checkSendPreconditions(String channelName) {
if (!RTMManager.instance.isInitialized) {
return 'RTM 未初始化,无法发送消息';
}
if (!RTMManager.instance.isLoggedIn) {
return 'RTM 未登录,无法发送消息';
}
if (channelName.isEmpty) {
return '未加入频道,无法发送消息';
}
return null;
}
///
///
/// [content]
/// [channelName] RTCManager
/// [showToast] Toast true
///
/// true false
Future<MessageSendResult> sendMessage({
required String content,
String? channelName,
bool showToast = true,
}) async {
//
final validationError = _validateMessage(content);
if (validationError != null) {
if (showToast) {
SmartDialog.showToast(validationError);
}
return MessageSendResult.failure(validationError);
}
//
final targetChannelName = channelName ??
RTCManager.instance.currentChannelId ?? '';
//
final preconditionError = _checkSendPreconditions(targetChannelName);
if (preconditionError != null) {
print('$preconditionError');
if (showToast) {
SmartDialog.showToast(preconditionError);
}
return MessageSendResult.failure(preconditionError);
}
try {
//
final messageData = _buildMessageData(content);
final messageJson = json.encode(messageData);
print('📤 发送消息到频道: $targetChannelName');
print('📤 消息内容: $messageJson');
// RTM
final success = await RTMManager.instance.publishChannelMessage(
channelName: targetChannelName,
message: messageJson,
);
if (success) {
print('✅ 消息发送成功');
//
final chatMessage = LiveChatMessage(
userId: messageData['userId'] as String,
userName: messageData['userName'] as String,
avatar: messageData['avatar'] as String?,
content: messageData['content'] as String,
timestamp: messageData['timestamp'] as int,
);
return MessageSendResult.success(chatMessage);
} else {
final error = '消息发送失败,请重试';
print('$error');
if (showToast) {
SmartDialog.showToast(error);
}
return MessageSendResult.failure(error);
}
} catch (e, stackTrace) {
final error = '发送消息异常: $e';
print('$error');
print('❌ 堆栈信息: $stackTrace');
if (showToast) {
SmartDialog.showToast(error);
}
return MessageSendResult.failure(error);
}
}
///
LiveChatMessage createLocalMessage(String content) {
final messageData = _buildMessageData(content);
return LiveChatMessage(
userId: messageData['userId'] as String,
userName: messageData['userName'] as String,
avatar: messageData['avatar'] as String?,
content: messageData['content'] as String,
timestamp: messageData['timestamp'] as int,
);
}
}
///
class MessageSendResult {
final bool success;
final LiveChatMessage? message;
final String? error;
MessageSendResult._({
required this.success,
this.message,
this.error,
});
factory MessageSendResult.success(LiveChatMessage message) {
return MessageSendResult._(
success: true,
message: message,
);
}
factory MessageSendResult.failure(String error) {
return MessageSendResult._(
success: false,
error: error,
);
}
}

32
lib/widget/live/live_room_action_bar.dart

@ -7,12 +7,14 @@ class LiveRoomActionBar extends StatelessWidget {
super.key,
required this.messageController,
required this.onMessageChanged,
required this.onSendTap,
required this.onGiftTap,
required this.onChargeTap,
});
final TextEditingController messageController;
final ValueChanged<String> onMessageChanged;
final VoidCallback onSendTap;
final VoidCallback onGiftTap;
final VoidCallback onChargeTap;
@ -51,7 +53,7 @@ class LiveRoomActionBar extends StatelessWidget {
),
child: TextField(
controller: messageController,
keyboardType: TextInputType.number,
keyboardType: TextInputType.text,
style: TextStyle(
fontSize: ScreenUtil().setWidth(14),
height: 1,
@ -69,22 +71,26 @@ class LiveRoomActionBar extends StatelessWidget {
border: InputBorder.none,
),
onChanged: onMessageChanged,
onSubmitted: (_) => onSendTap(),
),
),
),
SizedBox(width: 8.w),
Container(
width: 38.w,
height: 38.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(38.w)),
color: const Color.fromRGBO(0, 0, 0, .3),
),
child: Center(
child: Image.asset(
Assets.imagesArrowR,
width: 16.w,
height: 16.w,
InkWell(
onTap: onSendTap,
child: Container(
width: 38.w,
height: 38.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(38.w)),
color: const Color.fromRGBO(0, 0, 0, .3),
),
child: Center(
child: Image.asset(
Assets.imagesArrowR,
width: 16.w,
height: 16.w,
),
),
),
),

40
lib/widget/live/live_room_chat_item.dart

@ -1,9 +1,15 @@
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/model/live/live_chat_message.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class LiveRoomChatItem extends StatelessWidget {
const LiveRoomChatItem({super.key});
const LiveRoomChatItem({
super.key,
required this.message,
});
final LiveChatMessage message;
@override
Widget build(BuildContext context) {
@ -13,23 +19,43 @@ class LiveRoomChatItem extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.asset(Assets.imagesUserAvatar, width: 25.w, height: 25.w),
//
ClipOval(
child: message.avatar != null && message.avatar!.isNotEmpty
? Image.network(
message.avatar!,
width: 25.w,
height: 25.w,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
Assets.imagesUserAvatar,
width: 25.w,
height: 25.w,
);
},
)
: Image.asset(
Assets.imagesUserAvatar,
width: 25.w,
height: 25.w,
),
),
SizedBox(width: 10.w),
SizedBox(
width: 224.w,
//
Expanded(
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: "沙发沙发:",
text: "${message.userName}",
style: TextStyle(
fontSize: 11.w,
color: const Color.fromRGBO(155, 138, 246, 1),
),
),
TextSpan(
text:
"大家好啊!大家好啊!大家好啊!大家好啊!大家好啊!大家好啊!大家好啊!大家好啊!大家好啊!",
text: message.content,
style: TextStyle(fontSize: 11.w, color: Colors.white),
),
],

79
lib/widget/live/live_room_notice_chat_panel.dart

@ -1,13 +1,36 @@
import 'package:dating_touchme_app/controller/discover/room_controller.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/widget/live/live_room_chat_item.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class LiveRoomNoticeChatPanel extends StatelessWidget {
class LiveRoomNoticeChatPanel extends StatefulWidget {
const LiveRoomNoticeChatPanel({super.key});
@override
State<LiveRoomNoticeChatPanel> createState() => _LiveRoomNoticeChatPanelState();
}
class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
final RoomController controller = Get.find<RoomController>();
late final ScrollController scrollController;
@override
void initState() {
super.initState();
scrollController = ScrollController();
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
height: 230.w,
padding: EdgeInsets.only(left: 13.w, right: 9.w),
@ -15,29 +38,39 @@ class LiveRoomNoticeChatPanel extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
Text(
"欢迎来到直播间!严禁未成年人直播或礼物消费。严禁违法违规、低俗色情、吸烟酗酒、人身伤害等直播内容。理性消费如主播在直播中以不当方式诱导消费,请谨慎辨别。切勿私下交易,以防人身财产损失,谨防网络诈骗。",
style: TextStyle(
fontSize: 11.w,
color: const Color.fromRGBO(155, 138, 246, 1),
child: Obx(() {
//
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
return SingleChildScrollView(
controller: scrollController,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"欢迎来到直播间!严禁未成年人直播或礼物消费。严禁违法违规、低俗色情、吸烟酗酒、人身伤害等直播内容。理性消费如主播在直播中以不当方式诱导消费,请谨慎辨别。切勿私下交易,以防人身财产损失,谨防网络诈骗。",
style: TextStyle(
fontSize: 11.w,
color: const Color.fromRGBO(155, 138, 246, 1),
),
),
SizedBox(height: 15.w),
//
...controller.chatMessages.map(
(message) => LiveRoomChatItem(message: message),
),
),
SizedBox(height: 15.w),
const LiveRoomChatItem(),
const LiveRoomChatItem(),
const LiveRoomChatItem(),
const LiveRoomChatItem(),
const LiveRoomChatItem(),
const LiveRoomChatItem(),
const LiveRoomChatItem(),
const LiveRoomChatItem(),
const LiveRoomChatItem(),
],
),
),
],
),
);
}),
),
SizedBox(width: 18.w),
Image.asset(

27
lib/widget/live/live_room_user_header.dart

@ -38,11 +38,28 @@ class LiveRoomUserHeader extends StatelessWidget {
),
child: Row(
children: [
Image.asset(
avatarAsset,
width: 34.w,
height: 34.w,
),
//
avatarAsset.startsWith('http://') || avatarAsset.startsWith('https://')
? ClipOval(
child: Image.network(
avatarAsset,
width: 34.w,
height: 34.w,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Image.asset(
Assets.imagesUserAvatar,
width: 34.w,
height: 34.w,
);
},
),
)
: Image.asset(
avatarAsset,
width: 34.w,
height: 34.w,
),
SizedBox(width: 7.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,

Loading…
Cancel
Save