Browse Source

feat(message): 新增直播间邀请消息展示功能

- 在 MessageItem 中引入 RoomItem 组件
- 添加 `_isRoomInviteMessage` 方法用于识别直播间邀请消息
- 新增对自定义消息类型为 'live_room_invite' 的处理逻辑
- 创建 RoomItem 组件用于展示直播间邀请卡片
- 实现点击直播间卡片跳转至 LiveRoomPage 页面的功能
- 支持从自定义消息中解析房间信息(频道ID、主播昵称、头像)
- 添加消息重发机制,提升消息发送可靠性
- 使用 CachedNetworkImage 优化图片加载体验
- 增加时间标签与消息状态显示(发送中、失败重试)
ios
Jolie 4 months ago
parent
commit
00651a0ec6
2 changed files with 391 additions and 0 deletions
  1. 33
      lib/widget/message/message_item.dart
  2. 358
      lib/widget/message/room_item.dart

33
lib/widget/message/message_item.dart

@ -9,6 +9,7 @@ import 'voice_item.dart';
import 'video_item.dart';
import 'call_item.dart';
import 'gift_item.dart';
import 'room_item.dart';
import '../../controller/message/chat_controller.dart';
class MessageItem extends StatelessWidget {
@ -55,6 +56,19 @@ class MessageItem extends StatelessWidget {
return false;
}
//
bool _isRoomInviteMessage() {
try {
if (message.body.type == MessageType.CUSTOM) {
final customBody = message.body as EMCustomMessageBody;
return customBody.event == 'live_room_invite';
}
} catch (e) {
//
}
return false;
}
@override
Widget build(BuildContext context) {
print('📨 [MessageItem] 渲染消息,类型: ${message.body.type}');
@ -96,6 +110,25 @@ class MessageItem extends StatelessWidget {
},
);
}
//
if (message.body.type == MessageType.CUSTOM && _isRoomInviteMessage()) {
return RoomItem(
message: message,
isSentByMe: isSentByMe,
showTime: shouldShowTime(),
formattedTime: formatMessageTime(message.serverTime),
onResend: () {
// controller Get ChatController
try {
final controller = chatController ?? Get.find<ChatController>();
controller.resendMessage(message);
} catch (e) {
print('重发消息失败: $e');
}
},
);
}
//
if (message.body.type == MessageType.TXT) {

358
lib/widget/message/room_item.dart

@ -0,0 +1,358 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import 'package:get/get.dart';
import '../../generated/assets.dart';
import '../../pages/discover/live_room_page.dart';
import '../../controller/discover/room_controller.dart';
class RoomItem extends StatelessWidget {
final EMMessage message;
final bool isSentByMe;
final bool showTime;
final String formattedTime;
final VoidCallback? onResend;
const RoomItem({
required this.message,
required this.isSentByMe,
required this.showTime,
required this.formattedTime,
this.onResend,
super.key,
});
///
Map<String, String>? _parseRoomInfo() {
try {
if (message.body.type == MessageType.CUSTOM) {
final customBody = message.body as EMCustomMessageBody;
//
if (customBody.event == 'live_room_invite') {
return customBody.params;
}
}
} catch (e) {
print('解析房间信息失败: $e');
}
return null;
}
/// ID
String _getChannelId() {
final roomInfo = _parseRoomInfo();
return roomInfo?['channelId'] ?? '';
}
///
String _getAnchorName() {
final roomInfo = _parseRoomInfo();
return roomInfo?['anchorName'] ?? '主持人';
}
///
String _getAnchorAvatar() {
final roomInfo = _parseRoomInfo();
return roomInfo?['anchorAvatar'] ?? '';
}
///
void _handleTap() {
final channelId = _getChannelId();
if (channelId.isNotEmpty) {
// RoomController
final roomController = Get.isRegistered<RoomController>()
? Get.find<RoomController>()
: Get.put(RoomController());
//
roomController.joinChannel(channelId).then((_) {
Get.to(() => const LiveRoomPage(id: 0));
}).catchError((e) {
print('❌ 加入直播间失败: $e');
});
}
}
@override
Widget build(BuildContext context) {
final roomInfo = _parseRoomInfo();
if (roomInfo == null) {
return SizedBox.shrink();
}
final anchorName = _getAnchorName();
final anchorAvatar = _getAnchorAvatar();
return Column(
children: [
//
if (showTime) _buildTimeLabel(),
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Row(
mainAxisAlignment:
isSentByMe ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (!isSentByMe) _buildAvatar(),
if (!isSentByMe) SizedBox(width: 8.w),
//
if (isSentByMe)
Align(
alignment: Alignment.center,
child: Container(
margin: EdgeInsets.only(top: 10.h),
child: _buildMessageStatus(),
),
),
if (isSentByMe) SizedBox(width: 10.w),
//
GestureDetector(
onTap: _handleTap,
child: Container(
constraints: BoxConstraints(maxWidth: 250.w),
margin: EdgeInsets.only(top: 10.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Stack(
children: [
Container(
width: 250.w,
height: 250.w,
color: Colors.grey[200],
child: anchorAvatar.isNotEmpty
? CachedNetworkImage(
imageUrl: anchorAvatar,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
color: Colors.grey[200],
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.grey[600],
),
),
),
errorWidget: (context, url, error) =>
Container(
color: Colors.grey[200],
child: Icon(
Icons.live_tv,
size: 40.w,
color: Colors.grey[400],
),
),
)
: Container(
color: Colors.grey[200],
child: Icon(
Icons.live_tv,
size: 40.w,
color: Colors.grey[400],
),
),
),
//
Positioned(
top: 8.w,
left: 8.w,
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 4.w,
),
decoration: BoxDecoration(
color: const Color.fromRGBO(117, 98, 249, 1),
borderRadius: BorderRadius.circular(4.w),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.play_circle_filled,
size: 12.w,
color: Colors.white,
),
SizedBox(width: 4.w),
Text(
'直播中',
style: TextStyle(
fontSize: 11.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
],
),
//
Container(
padding: EdgeInsets.all(12.w),
child: Row(
children: [
//
ClipRRect(
borderRadius: BorderRadius.circular(12.w),
child: Container(
width: 24.w,
height: 24.w,
color: Colors.grey[300],
child: anchorAvatar.isNotEmpty
? CachedNetworkImage(
imageUrl: anchorAvatar,
width: 24.w,
height: 24.w,
fit: BoxFit.cover,
placeholder: (context, url) =>
Container(
color: Colors.grey[300],
child: Center(
child: SizedBox(
width: 12.w,
height: 12.w,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.grey[600],
),
),
),
),
errorWidget: (context, url, error) =>
Icon(
Icons.person,
size: 16.w,
color: Colors.grey[600],
),
)
: Icon(
Icons.person,
size: 16.w,
color: Colors.grey[600],
),
),
),
SizedBox(width: 8.w),
//
Expanded(
child: Text(
anchorName,
style: TextStyle(
fontSize: 13.sp,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
),
),
),
if (isSentByMe) SizedBox(width: 8.w),
if (isSentByMe) _buildAvatar(),
],
),
),
],
);
}
//
Widget _buildTimeLabel() {
return Container(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Text(
formattedTime,
style: TextStyle(fontSize: 12.sp, color: Colors.grey),
),
),
);
}
//
Widget _buildAvatar() {
return Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.w),
image: DecorationImage(
image: AssetImage(Assets.imagesAvatarsExample),
fit: BoxFit.cover,
),
),
);
}
//
Widget _buildMessageStatus() {
if (!isSentByMe) {
return SizedBox.shrink();
}
final status = message.status;
if (status == MessageStatus.FAIL) {
//
return GestureDetector(
onTap: onResend,
child: Container(
width: 20.w,
height: 20.w,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.refresh,
size: 14.w,
color: Colors.red,
),
),
);
} else if (status == MessageStatus.PROGRESS) {
//
return Container(
width: 16.w,
height: 16.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey),
),
);
} else {
//
return SizedBox.shrink();
}
}
}
Loading…
Cancel
Save