Browse Source

feat(message): 实现会话删除功能并优化UI交互

- 在ConversationController中新增deleteConversation方法,支持删除指定会话
- 在IMManager中实现deleteConversation底层逻辑,调用环信SDK删除会话
- 在会话列表页面添加侧滑删除功能,使用TDSwipeCell组件实现
-优化会话项布局和代码结构,提高可读性和维护性
- 更新依赖源为国内镜像地址,提升包加载速度
- 移除不必要的导入模块,精简代码体积
- 调整部分UI间距和样式,改善视觉效果
ios
Jolie 4 months ago
parent
commit
1639eb67f7
7 changed files with 626 additions and 255 deletions
  1. 22
      lib/controller/message/conversation_controller.dart
  2. 34
      lib/im/im_manager.dart
  3. 10
      lib/pages/home/home_page.dart
  4. 98
      lib/pages/message/conversation_tab.dart
  5. 278
      lib/rtc/rtm_manager.dart
  6. 438
      pubspec.lock
  7. 1
      pubspec.yaml

22
lib/controller/message/conversation_controller.dart

@ -1,4 +1,5 @@
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import '../../im/im_manager.dart';
@ -26,7 +27,8 @@ class ConversationController extends GetxController {
errorMessage.value = '';
// IMManager获取会话列表
final List<EMConversation> convList = await IMManager.instance.getConversations();
final List<EMConversation> convList = await IMManager.instance
.getConversations();
//
conversations.value = convList;
@ -115,4 +117,22 @@ class ConversationController extends GetxController {
Future<EMMessage?> lastMessage(EMConversation conversation) async{
return await conversation.latestMessage();
}
///
Future<bool> deleteConversation(String conversationId) async {
try {
final success = await IMManager.instance.deleteConversation(
conversationId,
);
if (success) {
conversations.removeWhere((element) => element.id == conversationId);
}
return success;
} catch (e) {
if (Get.isLogEnable) {
Get.log('删除会话失败: $e');
}
return false;
}
}
}

34
lib/im/im_manager.dart

@ -322,6 +322,28 @@ class IMManager {
}
///
///
Future<bool> deleteConversation(
String conversationId, {
bool deleteMessages = true,
}) async {
try {
await EMClient.getInstance.chatManager.deleteConversation(
conversationId,
deleteMessages: deleteMessages,
);
return true;
} catch (e) {
if (Get.isLogEnable) {
Get.log('删除会话失败: $e');
} else {
print('删除会话失败: $e');
}
return false;
}
}
///
Future<Map<String, EMUserInfo>> getContacts(String userId) async {
return await EMClient.getInstance.userInfoManager.fetchUserInfoById([
userId,
@ -336,21 +358,21 @@ class IMManager {
}) async {
try {
EMConversationType convType = EMConversationType.Chat;
//
final conversation = await EMClient.getInstance.chatManager.getConversation(
conversationId,
type: convType,
createIfNeed: false,
);
if (conversation == null) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 会话不存在: $conversationId');
}
return [];
}
// startMsgId
if (startMsgId != null && startMsgId.isNotEmpty) {
// ID开始加载更旧的消息
@ -542,7 +564,7 @@ class IMManager {
for (var message in messages) {
final fromId = message.from;
final toId = message.to;
// ChatController
if (fromId != null) {
final controller = _activeChatControllers[fromId];
@ -553,7 +575,7 @@ class IMManager {
}
}
}
// ChatController
if (toId != null) {
final controller = _activeChatControllers[toId];
@ -565,7 +587,7 @@ class IMManager {
}
}
}
//
_refreshConversationList();
} catch (e) {

10
lib/pages/home/home_page.dart

@ -6,11 +6,11 @@ import 'package:dating_touchme_app/pages/home/recommend_tab.dart';
import 'package:dating_touchme_app/pages/home/nearby_tab.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage>
with AutomaticKeepAliveClientMixin {

98
lib/pages/message/conversation_tab.dart

@ -1,9 +1,8 @@
import 'package:dating_touchme_app/extension/ex_widget.dart';
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 '../../controller/message/conversation_controller.dart';
@ -14,7 +13,8 @@ class ConversationTab extends StatefulWidget {
State<ConversationTab> createState() => _ConversationTabState();
}
class _ConversationTabState extends State<ConversationTab> with AutomaticKeepAliveClientMixin {
class _ConversationTabState extends State<ConversationTab>
with AutomaticKeepAliveClientMixin {
final ConversationController controller = Get.find<ConversationController>();
@override
@ -28,7 +28,7 @@ class _ConversationTabState extends State<ConversationTab> with AutomaticKeepAli
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.errorMessage.value.isNotEmpty) {
return Center(
child: Column(
@ -43,11 +43,11 @@ class _ConversationTabState extends State<ConversationTab> with AutomaticKeepAli
),
);
}
if (controller.conversations.isEmpty) {
return const Center(child: Text('暂无会话'));
}
return ListView.builder(
padding: const EdgeInsets.only(top: 8),
itemCount: controller.conversations.length,
@ -64,53 +64,67 @@ class _ConversationTabState extends State<ConversationTab> with AutomaticKeepAli
//
Widget _buildConversationItem(EMConversation conversation) {
// 使FutureBuilder获取未读消息数和最新消息
return FutureBuilder<EMUserInfo>(
future: controller.loadContact(conversation.id),
builder: (context, snapshot) {
EMUserInfo? userInfo = snapshot.data;
builder: (context, userSnapshot) {
final EMUserInfo? userInfo = userSnapshot.data;
return FutureBuilder<EMMessage?>(
future: controller.lastMessage(conversation),
builder: (context, snapshot) {
EMMessage? message = snapshot.data;
builder: (context, messageSnapshot) {
final EMMessage? message = messageSnapshot.data;
return FutureBuilder<int>(
future: controller.getUnreadCount(conversation),
builder: (context, snapshot){
int unreadCount = snapshot.data ?? 0;
return GestureDetector(
onTap: () async{
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),
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),
margin: const EdgeInsets.only(
bottom: 8,
left: 16,
right: 16,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
image: DecorationImage(
image: userInfo?.avatarUrl != '' ? NetworkImage(userInfo?.avatarUrl ?? '') : AssetImage(Assets.imagesAvatarsExample),
image: (userInfo?.avatarUrl ?? '').isNotEmpty
? NetworkImage(userInfo!.avatarUrl!)
: const AssetImage(
Assets.imagesAvatarsExample,
)
as ImageProvider,
fit: BoxFit.cover,
),
),
),
const SizedBox(width: 12),
//
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
userInfo?.nickName ?? '联系人',
@ -121,7 +135,9 @@ class _ConversationTabState extends State<ConversationTab> with AutomaticKeepAli
),
),
Text(
controller.formatMessageTime(message?.serverTime ?? 0),
controller.formatMessageTime(
message?.serverTime ?? 0,
),
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
@ -141,10 +157,12 @@ class _ConversationTabState extends State<ConversationTab> with AutomaticKeepAli
],
),
),
//
if (unreadCount > 0)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
@ -160,15 +178,39 @@ class _ConversationTabState extends State<ConversationTab> with AutomaticKeepAli
],
),
),
);
}
),
);
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,
);
},
);
},
);
},
);
}
@override
bool get wantKeepAlive => true;
}

278
lib/rtc/rtm_manager.dart

@ -0,0 +1,278 @@
import 'package:agora_rtm/agora_rtm.dart';
/// RTM StreamChannel
class RTMManager {
RTMManager._internal();
static final RTMManager _instance = RTMManager._internal();
factory RTMManager() => _instance;
static RTMManager get instance => _instance;
RtmClient? _client;
bool _isInitialized = false;
bool _isLoggedIn = false;
String? _currentAppId;
String? _currentUserId;
final Map<String, StreamChannel> _streamChannels = {};
/// link state
void Function(LinkStateEvent event)? onLinkStateEvent;
/// /
void Function(MessageEvent event)? onMessageEvent;
/// presence
void Function(PresenceEvent event)? onPresenceEvent;
/// topic
void Function(TopicEvent event)? onTopicEvent;
/// lock
void Function(LockEvent event)? onLockEvent;
/// storage
void Function(StorageEvent event)? onStorageEvent;
/// token
void Function(TokenEvent event)? onTokenEvent;
///
void Function(RtmStatus status)? onOperationError;
/// / RTM Client
Future<bool> initialize({
required String appId,
required String userId,
RtmConfig? config,
}) async {
if (_isInitialized &&
_client != null &&
_currentAppId == appId &&
_currentUserId == userId) {
return true;
}
await dispose();
final (status, client) = await RTM(appId, userId, config: config);
if (status.error) {
onOperationError?.call(status);
return false;
}
_client = client;
_currentAppId = appId;
_currentUserId = userId;
_isInitialized = true;
_registerClientListeners();
return true;
}
/// RTM
Future<bool> login(String token) async {
_ensureInitialized();
final (status, _) = await _client!.login(token);
final ok = _handleStatus(status);
if (ok) {
_isLoggedIn = true;
}
return ok;
}
/// RTM
Future<void> logout() async {
if (!_isInitialized || _client == null || !_isLoggedIn) return;
await leaveAllStreamChannels();
final (status, _) = await _client!.logout();
_handleStatus(status);
_isLoggedIn = false;
}
/// Token
Future<bool> renewToken(String token) async {
_ensureInitialized();
final (status, _) = await _client!.renewToken(token);
return _handleStatus(status);
}
/// JSON
Future<bool> setParameters(String paramsJson) async {
_ensureInitialized();
final status = await _client!.setParameters(paramsJson);
return _handleStatus(status);
}
///
Future<bool> publishChannelMessage({
required String channelName,
required String message,
RtmChannelType channelType = RtmChannelType.message,
String? customType,
bool storeInHistory = false,
}) async {
_ensureInitialized();
final (status, _) = await _client!.publish(
channelName,
message,
channelType: channelType,
customType: customType,
storeInHistory: storeInHistory,
);
return _handleStatus(status);
}
/// / StreamChannel
Future<bool> joinStreamChannel(
String channelName, {
String? token,
bool withMetadata = false,
bool withPresence = true,
bool withLock = false,
bool beQuiet = false,
}) async {
_ensureInitialized();
StreamChannel? channel = _streamChannels[channelName];
if (channel == null) {
final (createStatus, createdChannel) = await _client!.createStreamChannel(
channelName,
);
if (!_handleStatus(createStatus) || createdChannel == null) {
return false;
}
channel = createdChannel;
_streamChannels[channelName] = channel;
}
final (status, _) = await channel.join(
token: token,
withMetadata: withMetadata,
withPresence: withPresence,
withLock: withLock,
beQuiet: beQuiet,
);
return _handleStatus(status);
}
/// StreamChannel
Future<void> leaveStreamChannel(String channelName) async {
final channel = _streamChannels[channelName];
if (channel == null) return;
final (status, _) = await channel.leave();
_handleStatus(status);
await channel.release();
_streamChannels.remove(channelName);
}
/// StreamChannel
Future<void> leaveAllStreamChannels() async {
final names = _streamChannels.keys.toList();
for (final name in names) {
await leaveStreamChannel(name);
}
}
///
Future<bool> joinTopic({
required String channelName,
required String topic,
RtmMessageQos qos = RtmMessageQos.unordered,
RtmMessagePriority priority = RtmMessagePriority.normal,
String meta = '',
bool syncWithMedia = false,
}) async {
final channel = await _requireChannel(channelName);
final (status, _) = await channel.joinTopic(
topic,
qos: qos,
priority: priority,
meta: meta,
syncWithMedia: syncWithMedia,
);
return _handleStatus(status);
}
///
Future<bool> publishTopicMessage({
required String channelName,
required String topic,
required String message,
int sendTs = 0,
String? customType,
}) async {
final channel = await _requireChannel(channelName);
final (status, _) = await channel.publishTextMessage(
topic,
message,
sendTs: sendTs,
customType: customType,
);
return _handleStatus(status);
}
/// RTM Client
Future<void> dispose() async {
await leaveAllStreamChannels();
if (_client != null && _isLoggedIn) {
final (status, _) = await _client!.logout();
_handleStatus(status);
}
await _client?.release();
_client = null;
_isInitialized = false;
_isLoggedIn = false;
_currentAppId = null;
_currentUserId = null;
_streamChannels.clear();
}
bool get isInitialized => _isInitialized;
bool get isLoggedIn => _isLoggedIn;
String? get currentUserId => _currentUserId;
Iterable<String> get joinedStreamChannels => _streamChannels.keys;
void _registerClientListeners() {
if (_client == null) return;
_client!.addListener(
linkState: (event) => onLinkStateEvent?.call(event),
message: (event) => onMessageEvent?.call(event),
presence: (event) => onPresenceEvent?.call(event),
topic: (event) => onTopicEvent?.call(event),
lock: (event) => onLockEvent?.call(event),
storage: (event) => onStorageEvent?.call(event),
token: (event) => onTokenEvent?.call(event),
);
}
Future<StreamChannel> _requireChannel(String channelName) async {
if (!_streamChannels.containsKey(channelName)) {
final ok = await joinStreamChannel(channelName);
if (!ok) {
throw Exception('加入 StreamChannel 失败:$channelName');
}
}
final channel = _streamChannels[channelName];
if (channel == null) {
throw Exception('StreamChannel 不存在:$channelName');
}
return channel;
}
bool _handleStatus(RtmStatus status) {
if (status.error) {
onOperationError?.call(status);
return false;
}
return true;
}
void _ensureInitialized() {
if (!_isInitialized || _client == null) {
throw Exception('RTM Client 未初始化,请先调用 initialize');
}
}
}

438
pubspec.lock
File diff suppressed because it is too large
View File

1
pubspec.yaml

@ -67,6 +67,7 @@ dependencies:
tobias: ^5.3.1
agora_rtc_engine: ^6.5.3
pull_to_refresh: ^2.0.0
agora_rtm: ^2.2.5
dev_dependencies:
flutter_test:

Loading…
Cancel
Save