You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
395 lines
15 KiB
395 lines
15 KiB
import 'package:dating_touchme_app/im/im_manager.dart';
|
|
import 'package:dating_touchme_app/pages/message/chat_page.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:dating_touchme_app/generated/assets.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:tdesign_flutter/tdesign_flutter.dart';
|
|
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import '../../controller/message/conversation_controller.dart';
|
|
import '../../widget/message/emoji_text_widget.dart';
|
|
import '../../config/emoji_config.dart';
|
|
|
|
class ConversationTab extends StatefulWidget {
|
|
const ConversationTab({super.key});
|
|
|
|
@override
|
|
State<ConversationTab> createState() => _ConversationTabState();
|
|
}
|
|
|
|
class _ConversationTabState extends State<ConversationTab>
|
|
with AutomaticKeepAliveClientMixin {
|
|
final ConversationController controller = Get.find<ConversationController>();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
return Column(
|
|
children: [
|
|
// 聊天列表
|
|
Expanded(
|
|
child: Obx(() {
|
|
if (controller.isLoading.value) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
if (controller.errorMessage.value.isNotEmpty) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(controller.errorMessage.value),
|
|
ElevatedButton(
|
|
onPressed: () => controller.refreshConversations(),
|
|
child: const Text('重试'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 直接使用 Obx 监听 conversations 和 filterType,避免 FutureBuilder 重建导致的闪烁
|
|
return Obx(() {
|
|
final filteredConversations = controller.conversations;
|
|
|
|
if (filteredConversations.isEmpty) {
|
|
return Center(
|
|
child: Text(
|
|
controller.filterType.value == FilterType.none
|
|
? '暂无会话'
|
|
: '暂无符合条件的会话',
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
padding: const EdgeInsets.only(top: 8),
|
|
itemCount: filteredConversations.length,
|
|
itemBuilder: (context, index) {
|
|
final conversation = filteredConversations[index];
|
|
return BuildConversationItem(conversation: conversation);
|
|
},
|
|
);
|
|
});
|
|
}),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
}
|
|
|
|
|
|
class BuildConversationItem extends StatefulWidget {
|
|
final EMConversation conversation;
|
|
const BuildConversationItem({super.key, required this.conversation});
|
|
|
|
@override
|
|
State<BuildConversationItem> createState() => _BuildConversationItemState();
|
|
}
|
|
|
|
class _BuildConversationItemState extends State<BuildConversationItem> {
|
|
|
|
late EMConversation conversation;
|
|
|
|
bool isOnline = false;
|
|
late var cachedUserInfo;
|
|
|
|
|
|
final ConversationController controller = Get.find<ConversationController>();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
conversation = widget.conversation;
|
|
cachedUserInfo = controller.getCachedUserInfo(conversation.id);
|
|
IMManager.instance.getUserPresenceStatus(conversation.id).then((e){
|
|
isOnline = e == true;
|
|
setState(() {
|
|
|
|
});
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FutureBuilder<ExtendedUserInfo?>(
|
|
future: cachedUserInfo != null
|
|
? Future.value(cachedUserInfo)
|
|
: controller.loadContact(conversation.id),
|
|
initialData: cachedUserInfo,
|
|
builder: (context, userSnapshot) {
|
|
final ExtendedUserInfo? userInfo = userSnapshot.data;
|
|
return FutureBuilder<EMMessage?>(
|
|
future: controller.lastMessage(conversation),
|
|
builder: (context, messageSnapshot) {
|
|
final EMMessage? message = messageSnapshot.data;
|
|
return FutureBuilder<int>(
|
|
future: controller.getUnreadCount(conversation),
|
|
builder: (context, unreadSnapshot) {
|
|
final int unreadCount = unreadSnapshot.data ?? 0;
|
|
final double screenWidth = MediaQuery.of(context).size.width;
|
|
|
|
final Widget cellContent = Builder(
|
|
builder: (cellContext) => GestureDetector(
|
|
onTap: () async {
|
|
TDSwipeCellInherited.of(cellContext)?.cellClick();
|
|
Get.to(ChatPage(userId: conversation.id));
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
|
),
|
|
margin: const EdgeInsets.only(
|
|
bottom: 8,
|
|
left: 16,
|
|
right: 16,
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Stack(
|
|
children: [
|
|
Container(
|
|
width: 56,
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(28),
|
|
color: Colors.grey[300],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(28),
|
|
child: (userInfo?.avatarUrl != null && userInfo!.avatarUrl!.isNotEmpty)
|
|
? CachedNetworkImage(
|
|
imageUrl: userInfo.avatarUrl!,
|
|
width: 56,
|
|
height: 56,
|
|
fit: BoxFit.cover,
|
|
placeholder: (context, url) => Image.asset(
|
|
Assets.imagesUserAvatar,
|
|
width: 56,
|
|
height: 56,
|
|
fit: BoxFit.cover,
|
|
),
|
|
errorWidget: (context, url, error) => Image.asset(
|
|
Assets.imagesUserAvatar,
|
|
width: 56,
|
|
height: 56,
|
|
fit: BoxFit.cover,
|
|
),
|
|
)
|
|
: Image.asset(
|
|
Assets.imagesUserAvatar,
|
|
width: 56,
|
|
height: 56,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
),
|
|
if(isOnline) Positioned(
|
|
bottom: 0,
|
|
right: 0,
|
|
child: Container(
|
|
width: 11.w,
|
|
height: 11.w,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.all(Radius.circular(11.w)),
|
|
color: const Color.fromRGBO(43, 255, 191, 1)
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
userInfo?.nickName ?? conversation.id, // 如果没有昵称,显示用户ID
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
controller.formatMessageTime(
|
|
message?.serverTime ?? 0,
|
|
),
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Expanded(
|
|
child: _buildLastMessageContent(
|
|
controller.getLastMessageContent(message),
|
|
message,
|
|
),
|
|
),
|
|
if (unreadCount > 0)
|
|
TDBadge(TDBadgeType.message, count: unreadCount.toString(), maxCount: '99')
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
return TDSwipeCell(
|
|
slidableKey: ValueKey('conversation_${conversation.id}'),
|
|
groupTag: 'conversation_swipe_group',
|
|
right: TDSwipeCellPanel(
|
|
extentRatio: 72 / screenWidth,
|
|
children: [
|
|
TDSwipeCellAction(
|
|
backgroundColor: TDTheme.of(context).errorColor6,
|
|
label: '删除',
|
|
onPressed: (actionContext) async {
|
|
final success = await controller.deleteConversation(
|
|
conversation.id,
|
|
);
|
|
if (!success) {
|
|
Get.snackbar('提示', '删除会话失败');
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
cell: cellContent,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建最后一条消息内容(支持表情显示)
|
|
Widget _buildLastMessageContent(String content, EMMessage? message) {
|
|
// 检查是否是发送失败的消息
|
|
final isFailed = message != null &&
|
|
message.direction == MessageDirection.SEND &&
|
|
message.status == MessageStatus.FAIL;
|
|
|
|
// 检查是否包含表情
|
|
final containsEmoji = EmojiTextHelper.containsEmoji(content);
|
|
|
|
if (containsEmoji && !isFailed) {
|
|
// 如果包含表情且不是失败消息,使用自定义的单行表情文本 widget
|
|
return _buildSingleLineEmojiText(
|
|
content,
|
|
TextStyle(
|
|
fontSize: 14.sp,
|
|
color: Colors.grey,
|
|
),
|
|
);
|
|
} else {
|
|
// 如果不包含表情或是失败消息,使用普通 Text 显示
|
|
return Text(
|
|
content,
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
color: isFailed
|
|
? Color.fromRGBO(248, 85, 66, 1) // 发送失败时显示红色
|
|
: Colors.grey,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 构建单行表情文本(支持省略)
|
|
Widget _buildSingleLineEmojiText(String text, TextStyle textStyle) {
|
|
final RegExp emojiRegex = RegExp(r'\[emoji:(\d+)\]');
|
|
final List<InlineSpan> spans = [];
|
|
int lastMatchEnd = 0;
|
|
final matches = emojiRegex.allMatches(text);
|
|
|
|
for (final match in matches) {
|
|
// 添加表情之前的文本
|
|
if (match.start > lastMatchEnd) {
|
|
final textPart = text.substring(lastMatchEnd, match.start);
|
|
spans.add(TextSpan(text: textPart, style: textStyle));
|
|
}
|
|
|
|
// 添加表情图片(使用 WidgetSpan)
|
|
final emojiId = match.group(1);
|
|
if (emojiId != null) {
|
|
final emoji = EmojiConfig.getEmojiById(emojiId);
|
|
if (emoji != null) {
|
|
spans.add(WidgetSpan(
|
|
alignment: PlaceholderAlignment.middle,
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 2.w),
|
|
child: Image.asset(
|
|
emoji.path,
|
|
width: 16.w,
|
|
height: 16.w,
|
|
fit: BoxFit.contain,
|
|
),
|
|
),
|
|
));
|
|
} else {
|
|
// 如果表情不存在,显示原始文本
|
|
spans.add(TextSpan(text: match.group(0)!, style: textStyle));
|
|
}
|
|
}
|
|
|
|
lastMatchEnd = match.end;
|
|
}
|
|
|
|
// 添加剩余的文本
|
|
if (lastMatchEnd < text.length) {
|
|
final textPart = text.substring(lastMatchEnd);
|
|
spans.add(TextSpan(text: textPart, style: textStyle));
|
|
}
|
|
|
|
return RichText(
|
|
text: TextSpan(children: spans),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
);
|
|
}
|
|
}
|
|
|