From 539cf87c4655382f5dba41012e2f91a601fefb2d Mon Sep 17 00:00:00 2001 From: abu <3109389044@qq.com> Date: Sat, 13 Sep 2025 17:01:01 +0800 Subject: [PATCH] group --- lib/IM/controller/chat_controller.dart | 5 +- lib/IM/controller/chat_detail_controller.dart | 6 +- lib/IM/im_group_listeners.dart | 23 +- lib/IM/im_message.dart | 50 +- lib/IM/im_message_listeners.dart | 37 +- lib/IM/im_service.dart | 30 +- lib/IM/push_service.dart | 10 +- lib/components/empty_tip.dart | 27 + lib/components/my_confirm.dart | 68 +++ lib/components/network_or_asset_image.dart | 2 +- lib/pages/chat/chat.dart | 19 +- lib/pages/chat/chat_group.dart | 279 +++++---- lib/pages/chat/chat_no_friend.dart | 287 ++++++++-- lib/pages/chat/index.dart | 33 +- .../components/invite_action_sheet.dart | 264 +++++++++ .../components/member_action_sheet.dart | 302 ++++++++++ .../groupChat/components/set_group_info.dart | 74 +++ .../controller/group_detail_controller.dart | 148 +++++ lib/pages/groupChat/groupDetail.dart | 541 ++++++++++++++++++ lib/pages/groupChat/groupList.dart | 2 + lib/pages/groupChat/index.dart | 104 ++-- lib/pages/my/index.dart | 57 +- lib/pages/my/user_info.dart | 21 +- .../upload_video_page/upload_video_page.dart | 51 +- lib/service/http_config.dart | 4 +- lib/utils/index.dart | 11 + lib/utils/notification_banner.dart | 38 +- lib/utils/parse_message_summary.dart | 121 +++- lib/utils/permissions.dart | 55 +- 29 files changed, 2342 insertions(+), 327 deletions(-) create mode 100644 lib/components/empty_tip.dart create mode 100644 lib/components/my_confirm.dart create mode 100644 lib/pages/groupChat/components/invite_action_sheet.dart create mode 100644 lib/pages/groupChat/components/member_action_sheet.dart create mode 100644 lib/pages/groupChat/components/set_group_info.dart create mode 100644 lib/pages/groupChat/controller/group_detail_controller.dart create mode 100644 lib/pages/groupChat/groupDetail.dart diff --git a/lib/IM/controller/chat_controller.dart b/lib/IM/controller/chat_controller.dart index 4286cef..11f0fe2 100644 --- a/lib/IM/controller/chat_controller.dart +++ b/lib/IM/controller/chat_controller.dart @@ -171,8 +171,9 @@ class ChatController extends GetxController { ///构建陌生人消息菜单入口(更新时候传入对应的会话) Future getNoFriendData({V2TimConversation? csion}) async { // 检测会话列表是否已有陌生人消息菜单 - // final hasNoFriend = chatList.any((item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false); - final hasNoFriend = chatList.any((item) => item.isCustomAdmin!.contains(myConversationType.ConversationType.noFriend.name)); + final hasNoFriend = chatList + .where((item) => item.conversation.type == 1) // 只检测单聊天 type == 1 的会话 + .any((item) => item.isCustomAdmin?.contains(myConversationType.ConversationType.noFriend.name) ?? false); logger.w('检测是否存在nofriend入口:$hasNoFriend'); if (hasNoFriend) { // 已经有了入口 diff --git a/lib/IM/controller/chat_detail_controller.dart b/lib/IM/controller/chat_detail_controller.dart index 46d7369..7174194 100644 --- a/lib/IM/controller/chat_detail_controller.dart +++ b/lib/IM/controller/chat_detail_controller.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/utils/index.dart'; +import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; class ChatDetailController extends GetxController { @@ -10,10 +11,10 @@ class ChatDetailController extends GetxController { ChatDetailController({required this.id}); final ScrollController chatController = ScrollController(); - final RxList chatList = [].obs; final RxBool isFriend = true.obs; final RxInt followType = 0.obs; + final RxBool toolFlag = true.obs; // 工具栏使用权限(针对群聊的) void updateChatListWithTimeLabels(List originMessages) async { final idRes = await ImService.instance.selfUserId(); @@ -31,6 +32,9 @@ class ChatDetailController extends GetxController { if (i == originMessages.length - 1) { // 最后一条消息,加时间标签 needInsertLabel = true; + } else if (current.localCustomData == 'time_label' || current.elemType == MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS) { + // + needInsertLabel = true; } else { final next = originMessages[i + 1]; final nextTimestamp = next.timestamp ?? 0; diff --git a/lib/IM/im_group_listeners.dart b/lib/IM/im_group_listeners.dart index a34d225..c28f59b 100644 --- a/lib/IM/im_group_listeners.dart +++ b/lib/IM/im_group_listeners.dart @@ -1,4 +1,6 @@ +import 'package:get/get.dart'; import 'package:logger/logger.dart'; +import 'package:loopin/pages/groupChat/controller/group_detail_controller.dart'; import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_change_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_change_info.dart'; @@ -120,13 +122,28 @@ class ImGroupListeners { void onMemberEnter(String groupID, List memberList) {} // 成员离开群 - void onMemberLeave(String groupID, V2TimGroupMemberInfo member) {} + void onMemberLeave(String groupID, V2TimGroupMemberInfo member) { + if (Get.isRegistered()) { + final ctl = Get.find(); + ctl.init(); + } + } // 成员被邀请入群 - void onMemberInvited(String groupID, List memberList) {} + void onMemberInvited(String groupID, List memberList) { + if (Get.isRegistered()) { + final ctl = Get.find(); + ctl.init(); + } + } // 成员被踢出群 - void onMemberKicked(String groupID, List memberList) {} + void onMemberKicked(String groupID, List memberList) { + if (Get.isRegistered()) { + final ctl = Get.find(); + ctl.init(); + } + } // 成员信息变更 void onMemberInfoChanged(String groupID, List v2timGroupMemberChangeInfoList) {} diff --git a/lib/IM/im_message.dart b/lib/IM/im_message.dart index 45c8073..071d14d 100644 --- a/lib/IM/im_message.dart +++ b/lib/IM/im_message.dart @@ -22,13 +22,16 @@ class IMMessage { String? toUserID, String? groupID, String? cloudCustomData, + String? groupName, + bool isPush = true, + bool isExcludedFromUnreadCount = false, }) async { // 必须且只能设置一个:toUserID(单聊)或 groupID(群聊) if ((toUserID == null && groupID == null) || (toUserID != null && groupID != null)) { return ImResult( success: false, code: -1, - desc: "只能指定一个 receiver(toUserID)或 groupID", + desc: "只能指定一个receiver:toUserID或groupID", ); } if (cloudCustomData != null) { @@ -37,10 +40,15 @@ class IMMessage { // 解析消息类型 V2TimValueCallback sendRes; // final controller = Get.find(); - final myInfo = Get.find(); - logger.w('启用默认title:${myInfo.nickname.value}'); // 单聊 if (toUserID != null) { + final myInfo = Get.find(); + logger.w('启用默认title:${myInfo.nickname.value}'); + OfflinePushInfo offlinePushInfo = OfflinePushInfo( + title: myInfo.nickname.value, + desc: parseMessageSummary(msg), + ext: jsonEncode({"userID": myInfo.userID.value, "title": myInfo.nickname.value}), + ); sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage( message: msg, receiver: toUserID, @@ -53,31 +61,43 @@ class IMMessage { // }, groupID: "", priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT, - onlineUserOnly: false, - isExcludedFromUnreadCount: false, - isExcludedFromLastMessage: false, - needReadReceipt: false, - offlinePushInfo: OfflinePushInfo( - title: myInfo.nickname.value, - desc: parseMessageSummary(msg), - ext: jsonEncode({"userID": myInfo.userID.value, "title": myInfo.nickname.value}), - ), + onlineUserOnly: false, // 是否只推送在线用户,默认全推 + isExcludedFromUnreadCount: isExcludedFromUnreadCount, //默认记入 不计入未读,如果这里设置了,那么message中设置的将失效 + isExcludedFromLastMessage: false, // 不作为会话的最新消息 + isSupportMessageExtension: false, // 支持消息扩展 + isExcludedFromContentModeration: false, // 绕过内容审核 + needReadReceipt: false, // 已读回执 + offlinePushInfo: isPush ? offlinePushInfo : null, cloudCustomData: cloudCustomData, localCustomData: "", ); } else { // 群聊 + OfflinePushInfo offlinePushInfo = OfflinePushInfo( + title: groupName, + desc: parseMessageSummary(msg), + ext: jsonEncode({"groupID": groupID, "title": groupName ?? ''}), + ); sendRes = await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage( message: msg, receiver: "", groupID: groupID!, + // onSyncMsgID: (msgID) async { + // 这里立刻拿到消息ID,可以提前把这条消息展示到列表中(发送中状态)有时间再改吧 + // 根据类型,创建对应的elem; + // logger.w(msg.imageElem!.toLogString()); + // controller.chatList.add(msg.imageElem); + // controller.scrollToBottom(); + // }, priority: MessagePriorityEnum.V2TIM_PRIORITY_DEFAULT, onlineUserOnly: false, - isExcludedFromUnreadCount: false, + isExcludedFromUnreadCount: isExcludedFromUnreadCount, isExcludedFromLastMessage: false, + isSupportMessageExtension: false, + isExcludedFromContentModeration: false, needReadReceipt: false, - offlinePushInfo: OfflinePushInfo(title: '群聊消息', desc: parseMessageSummary(msg)), - cloudCustomData: "", + offlinePushInfo: isPush ? offlinePushInfo : null, + cloudCustomData: cloudCustomData, localCustomData: "", ); } diff --git a/lib/IM/im_message_listeners.dart b/lib/IM/im_message_listeners.dart index 557f55f..fc0950c 100644 --- a/lib/IM/im_message_listeners.dart +++ b/lib/IM/im_message_listeners.dart @@ -12,6 +12,8 @@ import 'package:loopin/utils/notification_banner.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; import 'package:tencent_cloud_chat_sdk/enum/V2TimAdvancedMsgListener.dart'; import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/receive_message_opt.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart'; import 'package:tencent_cloud_chat_sdk/tencent_im_sdk_plugin.dart'; @@ -71,13 +73,14 @@ class ImMessageListenerService extends GetxService { /// 处理消息 void _handleNewMessage(V2TimMessage message) async { - final id = message.sender ?? message.groupID ?? ''; + final id = Utils.handleText(message.groupID, message.sender, ''); // 优先取groupID,因为senderID是必然存在的; + if (id.isEmpty) return; final isGroup = message.groupID != null && message.groupID!.isNotEmpty; final conversationID = isGroup ? 'group_${message.groupID}' : 'c2c_${message.userID}'; if (id.isEmpty) return; /// 是否正在聊天 优先处理 - if ((Get.currentRoute == '/chat' || Get.currentRoute == '/chatNoFriend' || Get.currentRoute == '/chatGroup') && Get.isRegistered()) { + if (Get.isRegistered()) { final chatDetailController = Get.find(); // 单聊和群聊的处理(chatDetailController.id是通过chat_binding传入的,按顺序取userID,没有则取groupID) if (chatDetailController.id == id) { @@ -88,6 +91,13 @@ class ImMessageListenerService extends GetxService { } return; } + //如果是tips类型的 + if (message.elemType == MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS) { + // 如果是群tips 不展示消息提示 + await ImService.instance.clearConversationUnreadCount(conversationID: conversationID); + return; + } + // 未杀死状态,交给在线推送处理 if (!LifecycleHandler.isInForeground) { logger.i("App 当前在后台,收到来自 $id 的消息 ${message.msgID},暂不展示弹窗"); @@ -97,9 +107,27 @@ class ImMessageListenerService extends GetxService { //TODO 等消息发送写完以后处理消息免打扰过滤,实现:发送消息时依赖cloudCustomData,根据conversation设置的opty属性是否开启了过滤,给customdata赋值 //TODO 群消息 + ///--------免打扰相关 + // 单聊消息接收选项c2CReceiveMessageOpt,先不管单聊 + // 如果是群,先拿群的信息recvOpt + if (isGroup) { + // + final res = await ImService.instance.getGroupsInfo(groupIDList: [message.groupID!]); + if (res.success && res.data != null) { + final V2TimGroupInfo groupData = res.data!.first.groupInfo!; + if (groupData.recvOpt == ReceiveMsgOptType.kTIMRecvMsgOpt_Not_Notify_Except_At) { + // 开启了免打扰模式, 现在只做了0=初始正常接受,3=在线接受,离线只接受@消息 + return; + } + } else { + // 获取失败默认也不提示; + return; + } + } + _debounceTimer?.cancel(); // 每次都取消旧的 _debounceTimer = Timer(Duration(milliseconds: 1000), () { - NotificationBanner.show(message); + NotificationBanner.show(message, isGroup); }); } @@ -156,8 +184,7 @@ class ImMessageListenerService extends GetxService { onRecvMessageModified: (V2TimMessage message) { logger.i("消息被修改: ${message.msgID}"); // 目前就红包领取状态的变更 - if ((Get.currentRoute == '/chat' || Get.currentRoute == '/chatNoFriend' || Get.currentRoute == '/chatGroup') && - Get.isRegistered()) { + if (Get.isRegistered()) { final controller = Get.find(); final index = controller.chatList.indexWhere((m) => m.msgID == message.msgID); if (index != -1) { diff --git a/lib/IM/im_service.dart b/lib/IM/im_service.dart index 718b6a1..577f17a 100644 --- a/lib/IM/im_service.dart +++ b/lib/IM/im_service.dart @@ -21,6 +21,7 @@ import 'package:tencent_cloud_chat_sdk/enum/group_application_type_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/group_member_filter_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/group_member_role_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/history_msg_get_type_enum.dart'; +import 'package:tencent_cloud_chat_sdk/enum/receive_message_opt_enum.dart'; import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; @@ -749,6 +750,30 @@ class ImService { return ImResult.wrap(res); } + /// 设置群消息免打扰 + Future> setGroupReceiveMessageOpt({ + required String groupID, + required ReceiveMsgOptEnum opt, + }) async { + final res = await TIMMessageManager.instance.setGroupReceiveMessageOpt( + groupID: groupID, + opt: opt, + ); + return ImResult.wrapNoData(res); + } + + /// 设置单聊消息免打扰 + Future> setC2CReceiveMessageOpt({ + required List userIDList, + required ReceiveMsgOptEnum opt, + }) async { + final res = await TIMMessageManager.instance.setC2CReceiveMessageOpt( + userIDList: userIDList, + opt: opt, + ); + return ImResult.wrapNoData(res); + } + ///------------------------------------ /// 创建群 Future> createGroup({ @@ -839,20 +864,17 @@ class ImService { /// - V2TIM_GROUP_MEMBER_FILTER_COMMON:普通群成员 /// [nextSeq] 分页拉取标志,首次传 0 /// [count] 拉取数量,最大 100 - /// [offset] 偏移量(仅 Web 端需要) Future> getGroupMemberList({ required String groupID, required GroupMemberFilterTypeEnum filter, required String nextSeq, int count = 20, - int offset = 0, }) async { final res = await V2TIMGroupManager().getGroupMemberList( groupID: groupID, filter: filter, nextSeq: nextSeq, count: count, - offset: offset, ); return ImResult.wrap(res); } @@ -893,7 +915,7 @@ class ImService { return ImResult.wrapNoData(res); } - /// 邀请他人入群(work群的入群方式,任何人可邀请人入群,无法做权限设置) + /// 邀请他人入群(work群的入群方式,任何人可邀请人入群) /// /// [groupID] 群 ID /// [userList] 被邀请成员的 userID 列表 diff --git a/lib/IM/push_service.dart b/lib/IM/push_service.dart index 5a1fb2c..07266c4 100644 --- a/lib/IM/push_service.dart +++ b/lib/IM/push_service.dart @@ -135,7 +135,7 @@ class PushService { logger.w(router); if (router == null || router != '') { // 聊天 - if (data['userID'] != null || data['userID'] != '') { + if (data['userID'] != null) { logger.w('有userID'); // 单聊,获取会话 final covRes = await ImService.instance.getConversation(conversationID: 'c2c_${data['userID']}'); @@ -151,13 +151,15 @@ class PushService { } } else { logger.w('没有userID'); - // 群聊消息 final groupRes = await ImService.instance.getConversation(conversationID: 'group_${data['groupID']}'); - Get.toNamed('/chatGroup', arguments: groupRes.data); + if (groupRes.success) { + final V2TimConversation groupConv = groupRes.data; + Get.toNamed('/chatGroup', arguments: groupConv); + } } } else { - // 通知类相关 + // 通知类相关(这里的ID只在特定业务场景才有,比如用户发起退款,要推送给商家,这里的id对应的就是订单id) Get.toNamed('/$router', arguments: data['id'] ?? ''); } } catch (e) { diff --git a/lib/components/empty_tip.dart b/lib/components/empty_tip.dart new file mode 100644 index 0000000..d5a8a53 --- /dev/null +++ b/lib/components/empty_tip.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class EmptyTip extends StatelessWidget { + final String text; + + const EmptyTip({super.key, this.text = '暂无数据'}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + 'assets/images/empty.png', + width: 100, + ), + const SizedBox(height: 8), + Text( + text, + style: const TextStyle(color: Colors.grey, fontSize: 13), + ), + ], + ), + ); + } +} diff --git a/lib/components/my_confirm.dart b/lib/components/my_confirm.dart new file mode 100644 index 0000000..dbf8784 --- /dev/null +++ b/lib/components/my_confirm.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class MyConfirm extends StatelessWidget { + final String title; + final String content; + final String confirmText; + final String cancelText; + + const MyConfirm({ + super.key, + required this.title, + required this.content, + this.confirmText = "确认", + this.cancelText = "取消", + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + title: Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + content: Text(content), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), // 关闭并返回 false + child: Text(cancelText), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onPressed: () => Get.back(result: true), // 关闭并返回 true + child: Text(confirmText), + ), + ], + ); + } +} + +class ConfirmDialog { + static Future show({ + required String title, + required String content, + String confirmText = "确认", + String cancelText = "取消", + }) { + return Get.dialog( + MyConfirm( + title: title, + content: content, + confirmText: confirmText, + cancelText: cancelText, + ), + barrierDismissible: false, // 点击外部不可关闭 + ); + } +} diff --git a/lib/components/network_or_asset_image.dart b/lib/components/network_or_asset_image.dart index 013447e..00ca5b7 100644 --- a/lib/components/network_or_asset_image.dart +++ b/lib/components/network_or_asset_image.dart @@ -32,7 +32,7 @@ class NetworkOrAssetImage extends StatelessWidget { } // 显示占位 return Image.asset( - placeholderAsset, + placeholderAsset.isEmpty ? 'assets/images/avatar/default.png' : placeholderAsset, width: width, height: height, fit: fit, diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 0a94923..909b6a0 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -297,8 +297,25 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ), )); } + // 写入记录的tips + else if (item.cloudCustomData == 'tips') { + msgtpl.add( + Container( + margin: const EdgeInsets.only(bottom: 15.0), + width: double.infinity, + child: Text( + item.textElem!.text ?? '', + style: TextStyle(color: Colors.grey[600], fontSize: 12.0), + softWrap: true, + maxLines: 5, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ); + } // 文本消息模板=1 - else if (item.elemType == 1) { + else if (item.elemType == 1 && item.cloudCustomData != 'tips') { msgtpl.add( RenderChatItem( data: item, diff --git a/lib/pages/chat/chat_group.dart b/lib/pages/chat/chat_group.dart index ca5b4ae..77343e5 100644 --- a/lib/pages/chat/chat_group.dart +++ b/lib/pages/chat/chat_group.dart @@ -15,7 +15,10 @@ import 'package:loopin/components/image_viewer.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/components/preview_video.dart'; import 'package:loopin/models/summary_type.dart'; +import 'package:loopin/pages/groupChat/controller/group_detail_controller.dart'; +import 'package:loopin/pages/groupChat/groupDetail.dart'; import 'package:loopin/utils/audio_player_service.dart'; +import 'package:loopin/utils/parse_message_summary.dart'; import 'package:loopin/utils/snapshot.dart'; import 'package:loopin/utils/voice_service.dart'; import 'package:mime/mime.dart'; @@ -25,7 +28,6 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; import 'package:video_player/video_player.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; -import '../../styles/index.dart'; import '../../utils/index.dart'; import './components/redpacket.dart'; import './components/richtext.dart'; @@ -105,6 +107,8 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix ); animTurns = Tween(begin: 0, end: 3.1415926).animate(animController); + isInGroup(); + cleanUnRead(); getUserId(); @@ -233,8 +237,25 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix ), )); } + // 写入记录的tips + else if (item.cloudCustomData == 'tips') { + msgtpl.add( + Container( + margin: const EdgeInsets.only(bottom: 15.0), + width: double.infinity, + child: Text( + item.textElem!.text ?? '', + style: TextStyle(color: Colors.grey[600], fontSize: 12.0), + softWrap: true, + maxLines: 5, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ); + } // 文本消息模板=1 - else if (item.elemType == 1) { + else if (item.elemType == 1 && item.cloudCustomData != 'tips') { msgtpl.add( RenderChatItem( data: item, @@ -758,6 +779,23 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix ), )); } + // 群tips + else if (item.elemType == 9) { + msgtpl.add( + Container( + margin: const EdgeInsets.only(bottom: 15.0), + width: double.infinity, + child: Text( + parseMessageSummary(item), + style: TextStyle(color: Colors.grey[600], fontSize: 12.0), + softWrap: true, + maxLines: 5, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ); + } } return msgtpl; } @@ -1017,7 +1055,7 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix late final ImResult res; res = await IMMessage().sendMessage( msg: message, - toUserID: arguments.groupID, + groupID: arguments.groupID, ); if (res.success && res.data != null) { @@ -1028,9 +1066,9 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix // controller.chatList.addAll(messagesToInsert); controller.scrollToBottom(); - print('发送成功'); + logger.w('发送成功'); } else { - print('消息发送失败: ${res.code} - ${res.desc}'); + logger.w('消息发送失败: ${res.code} - ${res.desc}'); } } @@ -1170,8 +1208,7 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix Future showPicker(BuildContext context) async { showModalBottomSheet( context: context, - backgroundColor: Colors.white, // 透明底色,这样圆角才能生效 - + backgroundColor: Colors.white, builder: (context) { return SafeArea( child: Column( @@ -1532,7 +1569,7 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix context: context, builder: (context) { return RedPacket( - flag: false, + flag: true, onSend: (date) { sendHongbao(date); }); @@ -1540,10 +1577,23 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix ); } + Future isInGroup() async { + final res = await ImService.instance.getJoinedGroupList(); + if (res.success && res.data != null) { + controller.toolFlag.value = res.data!.any((group) => group.groupID == arguments.groupID); + } else { + logger.e('获取数据失败:${res.desc}'); + // 禁止 + controller.toolFlag.value = false; + } + logger.w('当前是否可以使用工具栏:${controller.toolFlag.value}'); + } + /* ---------- { 其它功能模块 } ---------- */ @override Widget build(BuildContext context) { + //editorFocusNode return Stack( fit: StackFit.expand, children: [ @@ -1581,113 +1631,26 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix )), ), actions: [ - IconButton( - icon: const Icon( - Icons.more_horiz, - color: Colors.white, - ), - onPressed: () async { - final paddingTop = MediaQuery.of(Get.context!).padding.top; - - final selected = await showMenu( - context: Get.context!, - position: RelativeRect.fromLTRB( - double.infinity, - kToolbarHeight + paddingTop - 12, - 8, - double.infinity, - ), - color: FStyle.primaryColor, - elevation: 8, - items: [ - PopupMenuItem( - value: 'remark', - child: Row( - children: [ - Icon(Icons.edit, color: Colors.white, size: 18), - SizedBox(width: 8), - Text( - '设置备注', - style: TextStyle(color: Colors.white), - ), - ], + Obx( + () => controller.toolFlag.value + ? IconButton( + icon: const Icon( + Icons.menu, + color: Colors.white, ), - ), - PopupMenuItem( - value: 'not', - child: Row( - children: [ - Icon(Icons.do_not_disturb_on, color: Colors.white, size: 18), - SizedBox(width: 8), - Text( - '设为免打扰', - style: TextStyle(color: Colors.white), - ), - ], - ), - ), - PopupMenuItem( - value: 'report', - child: Row( - children: [ - Icon(Icons.report, color: Colors.white, size: 18), - SizedBox(width: 8), - Text( - '举报', - style: TextStyle(color: Colors.white), - ), - ], - ), - ), - PopupMenuItem( - value: 'block', - child: Row( - children: [ - Icon(Icons.block, color: Colors.white, size: 18), - SizedBox(width: 8), - Text( - '拉黑', - style: TextStyle(color: Colors.white), - ), - ], - ), - ), - PopupMenuItem( - value: 'foucs', - child: Row( - children: [ - Icon(Icons.person_remove_alt_1, color: Colors.white, size: 18), - SizedBox(width: 8), - Text( - '取消关注', - style: TextStyle(color: Colors.white), - ), - ], - ), - ), - ], - ); - - if (selected != null) { - switch (selected) { - case 'remark': - print('点击了备注'); - break; - case 'not': - print('点击了免打扰'); - break; - case 'report': - print('点击了举报'); - break; - case 'block': - print('点击了拉黑'); - break; - case 'foucs': - print('点击了取关'); - break; - } - } - }, + onPressed: () async { + editorFocusNode.unfocus(); + final ctl = Get.put(GroupDetailController(groupID: arguments.groupID ?? '')); + await ctl.init(); + final groupName = await Get.to(() => Groupdetail()); + if (groupName is String && groupName.isNotEmpty) { + setState(() { + arguments.showName = groupName; + }); + } + }, + ) + : SizedBox(), ), ], ), @@ -1744,6 +1707,7 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix padding: const EdgeInsets.all(10.0), child: Row( children: [ + // 语音按钮 InkWell( child: Icon( voiceBtnEnable ? Icons.keyboard_outlined : Icons.contactless_outlined, @@ -1751,16 +1715,22 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix size: 30.0, ), onTap: () { - setState(() { - toolbarEnable = false; - if (voiceBtnEnable) { - voiceBtnEnable = false; - editorFocusNode.requestFocus(); - } else { - voiceBtnEnable = true; - editorFocusNode.unfocus(); - } - }); + logger.w('点击了语音按钮'); + if (controller.toolFlag.value) { + setState(() { + toolbarEnable = false; + if (voiceBtnEnable) { + voiceBtnEnable = false; + editorFocusNode.requestFocus(); + // editorFocusNode.unfocus(); + } else { + voiceBtnEnable = true; + editorFocusNode.unfocus(); + } + }); + } else { + logger.e('退出群聊无法使用'); + } }, ), const SizedBox( @@ -1778,21 +1748,25 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix // 输入框 Offstage( offstage: voiceBtnEnable, - child: TextField( - decoration: const InputDecoration( - isDense: true, - hoverColor: Colors.transparent, - contentPadding: EdgeInsets.all(8.0), - border: OutlineInputBorder(borderSide: BorderSide.none), + child: Obx( + () => TextField( + enabled: controller.toolFlag.value, //true=禁止 + decoration: InputDecoration( + hintText: controller.toolFlag.value ? null : "你已退出群聊", + isDense: true, + hoverColor: Colors.transparent, + contentPadding: EdgeInsets.all(8.0), + border: OutlineInputBorder(borderSide: BorderSide.none), + ), + style: const TextStyle( + fontSize: 16.0, + ), + maxLines: null, + controller: editorController, + focusNode: editorFocusNode, + cursorColor: const Color(0xFF07C160), + onChanged: (value) {}, ), - style: const TextStyle( - fontSize: 16.0, - ), - maxLines: null, - controller: editorController, - focusNode: editorFocusNode, - cursorColor: const Color(0xFF07C160), - onChanged: (value) {}, ), ), // 语音 @@ -1870,6 +1844,7 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix const SizedBox( width: 10.0, ), + // 表情 InkWell( child: const Icon( Icons.add_reaction_rounded, @@ -1877,12 +1852,17 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix size: 30.0, ), onTap: () { - handleEmojChooseState(0); + if (controller.toolFlag.value) { + handleEmojChooseState(0); + } else { + logger.w('退出群聊禁止使用'); + } }, ), const SizedBox( width: 8.0, ), + // +号 InkWell( child: const Icon( Icons.add, @@ -1890,12 +1870,17 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix size: 30.0, ), onTap: () { - handleEmojChooseState(1); + if (controller.toolFlag.value) { + handleEmojChooseState(1); + } else { + logger.w('退出群聊禁止使用'); + } }, ), const SizedBox( width: 8.0, ), + // 发送按钮 InkWell( child: Container( height: 25.0, @@ -1911,7 +1896,11 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix ), ), onTap: () { - handleSubmit(); + if (controller.toolFlag.value) { + handleSubmit(); + } else { + logger.w('退出群聊禁止使用'); + } }, ), ], @@ -2150,10 +2139,10 @@ class _ChatGroupState extends State with SingleTickerProviderStateMix child: Visibility( visible: !voiceToTransfer, child: Align( - child: Text( - voiceTypeMap[voiceType], - style: const TextStyle(color: Colors.white70), - ), + child: Obx(() => Text( + controller.toolFlag.value ? voiceTypeMap[voiceType] : '无法在已退出的群聊发送消息', + style: const TextStyle(color: Colors.white70), + )), ), ), ), diff --git a/lib/pages/chat/chat_no_friend.dart b/lib/pages/chat/chat_no_friend.dart index 02c2f4e..95776d8 100644 --- a/lib/pages/chat/chat_no_friend.dart +++ b/lib/pages/chat/chat_no_friend.dart @@ -1,6 +1,8 @@ /// 聊天模板 library; +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/controller/chat_controller.dart'; @@ -8,10 +10,13 @@ import 'package:loopin/IM/controller/chat_detail_controller.dart'; import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_result.dart'; import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/components/image_viewer.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/components/preview_video.dart'; import 'package:loopin/models/conversation_type.dart'; +import 'package:loopin/models/summary_type.dart'; import 'package:loopin/pages/chat/notify_controller/notify_no_friend_controller.dart'; +import 'package:loopin/utils/audio_player_service.dart'; import 'package:loopin/utils/snapshot.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; @@ -294,8 +299,25 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt ), )); } + // 写入记录的tips + else if (item.cloudCustomData == 'tips') { + msgtpl.add( + Container( + margin: const EdgeInsets.only(bottom: 15.0), + width: double.infinity, + child: Text( + item.textElem!.text ?? '', + style: TextStyle(color: Colors.grey[600], fontSize: 12.0), + softWrap: true, + maxLines: 5, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ); + } // 文本消息模板=1 - else if (item.elemType == 1) { + else if (item.elemType == 1 && item.cloudCustomData != 'tips') { msgtpl.add( RenderChatItem( data: item, @@ -311,9 +333,9 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt padding: const EdgeInsets.all(10.0), child: RichTextUtil.getRichText(item.textElem?.text ?? '', color: !(item.isSelf ?? false) ? Colors.black : Colors.white), // 可自定义解析emoj/网址/电话 ), - // onLongPress: () { - // contextMenuDialog(); - // }, + onLongPress: () { + contextMenuDialog(); + }, ), ), ), @@ -350,6 +372,13 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt data: item, child: Ink( child: InkWell( + onTap: () { + // 预览图片 + Get.to(() => ImageViewer( + images: [imagePaths.first], + index: 0, + )); + }, overlayColor: WidgetStateProperty.all(Colors.transparent), child: ClipRRect( borderRadius: BorderRadius.circular(10.0), @@ -407,17 +436,9 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt children: [ ClipRRect( borderRadius: BorderRadius.circular(10.0), - child: Image.network( - fit: BoxFit.cover, - item.videoElem?.snapshotUrl ?? '', - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/pic1.jpg', - height: 60.0, - width: 60.0, - fit: BoxFit.cover, - ); - }, + child: NetworkOrAssetImage( + imageUrl: item.videoElem?.snapshotUrl ?? '', + width: 120, ), ), const Align( @@ -431,9 +452,6 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt ], ), ), - // onTap: () { - // MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); - // }, onTap: () { showGeneralDialog( context: context, @@ -463,6 +481,10 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt } // 语音模板=4 else if (item.elemType == 4) { + final durationMs = item.soundElem?.duration ?? 0; + final durationSeconds = (durationMs / 1000).round(); + final maxWidth = (durationSeconds / 60 * 230).clamp(80.0, 230.0); + List audiobody = [ Ink( decoration: BoxDecoration( @@ -475,8 +497,8 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt child: Container( padding: const EdgeInsets.all(10.0), constraints: BoxConstraints( - // maxWidth: 120.0, - maxWidth: (item.soundElem?.duration)! / 60 * 230, + maxWidth: maxWidth, + // maxWidth: (item.soundElem!.duration! / 1000) / 60 * 230, ), child: Row( mainAxisAlignment: !(item.isSelf ?? false) ? MainAxisAlignment.start : MainAxisAlignment.end, @@ -486,10 +508,10 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt const SizedBox( width: 5.0, ), - Text('${item.soundElem?.duration}'), + Text('$durationSeconds"'), ] : [ - Text('${item.soundElem?.duration}'), + Text('$durationSeconds"'), const SizedBox( width: 5.0, ), @@ -498,7 +520,15 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt ), ), onTap: () { - MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + final locUrl = item.soundElem?.path ?? ''; + final netUrl = item.soundElem?.url ?? ''; + if (locUrl.isNotEmpty) { + AudioPlayerService().playNetwork(locUrl); + } else if (netUrl.isNotEmpty) { + AudioPlayerService().playLocal(netUrl); + } else { + MyDialog.toast('音频文件已过期', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + } }, onLongPress: () { contextMenuDialog(); @@ -508,7 +538,8 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt const SizedBox( width: 5.0, ), - FStyle.badge(0, isdot: true), + + // FStyle.badge(0, isdot: true), ]; if (item.isSelf ?? false) { @@ -525,8 +556,177 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt children: audiobody, ))); } + // 分享团购商品 + else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareTuangou) { + // final makeJson = jsonEncode({ + // "price": shopObj['price'], + // "title": shopObj['name'], + // "url": shopObj['pic'], + // "sell": Utils.graceNumber(int.parse(shopObj['sales'] ?? '0')), + // "goodsId": shopObj['id'], + // "userID": Get.find().userID.value, + // }); + final obj = jsonDecode(item.customElem!.data!); + logger.e(obj); + final goodsId = obj['goodsId']; + final userID = obj['userID']; + final url = obj['url']; + final title = obj['title']; + final price = obj['price']; + final sell = Utils.graceNumber(int.tryParse(obj['sell'])!); + msgtpl.add(RenderChatItem( + data: item, + child: GestureDetector( + onTap: () { + // 这里带上分享人的ID + Get.toNamed('/goods', arguments: {'goodsId': goodsId, 'userID': userID}); + }, + child: Container( + width: 160, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(5), + offset: Offset(0.0, 1.0), + blurRadius: 1.0, + spreadRadius: 0.0, + ), + ]), + child: Column( + children: [ + NetworkOrAssetImage( + imageUrl: url, + width: 160.0, + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 5.0, + children: [ + Text( + '$title', + style: TextStyle(fontSize: 14.0, height: 1.2), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text.rich( + overflow: TextOverflow.ellipsis, + maxLines: 1, + TextSpan(style: TextStyle(color: Colors.red, fontSize: 12.0, fontWeight: FontWeight.w700, fontFamily: 'Arial'), children: [ + TextSpan(text: '¥'), + TextSpan( + text: '$price', + style: TextStyle( + fontSize: 14.0, + )), + ]), + ), + ), + SizedBox( + width: 5, + ), + Text( + '已售$sell件', + style: TextStyle(color: Colors.grey, fontSize: 10.0), + ), + ], + ), + ], + ), + ) + ], + ), + ), + ), + )); + } + // 分享短视频 + else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareVideo) { + /// {imgUrl,videoUrl,width,height} + final obj = jsonDecode(item.customElem!.data!); + logger.e(obj); + final videoId = obj['videoId']; + final videoUrl = obj['videoUrl']; + final imgUrl = obj['imgUrl']; + final width = obj['width'] as num; + final height = obj['height'] as num; + final isHorizontal = width > height; + msgtpl.add(RenderChatItem( + data: item, + child: Ink( + child: InkWell( + overlayColor: WidgetStateProperty.all(Colors.transparent), + child: SizedBox( + width: 120.0, + child: Stack( + alignment: Alignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10.0), + child: Container( + width: 120, + height: 240, + color: Colors.black, + child: NetworkOrAssetImage( + imageUrl: imgUrl, + fit: isHorizontal ? BoxFit.contain : BoxFit.cover, + placeholderAsset: 'assets/images/bk.jpg', + ), + ), + ), + const Align( + alignment: Alignment.center, + child: Icon( + Icons.play_circle, + color: Colors.white, + size: 30.0, + ), + ), + ], + ), + ), + onTap: () { + Get.toNamed('/videoDetail', arguments: {'videoId': videoId}); + // showGeneralDialog( + // context: context, + // barrierColor: Colors.black.withAlpha((1.0 * 255).round()), + // pageBuilder: (_, __, ___) { + // return SafeArea( + // bottom: true, + // child: Padding( + // padding: const EdgeInsets.only(bottom: 4), + // child: PreviewVideo( + // videoUrl: videoUrl, + // width: width.toDouble(), + // height: height.toDouble(), + // ), + // ), + // ); + // }, + // transitionBuilder: (_, anim, __, child) { + // return FadeTransition(opacity: anim, child: child); + // }, + // transitionDuration: const Duration(milliseconds: 200), + // ); + }, + // onLongPress: () { + // contextMenuDialog(); + // }, + ), + ), + )); + } // 红包模板=自定义=2; - else if (item.elemType == 2 && item.cloudCustomData == 'hongbao') { + else if (item.elemType == 2 && item.cloudCustomData == SummaryType.hongbao) { + final obj = jsonDecode(item.customElem!.data!); + final open = obj['open'] ?? false; + final remark = obj['remark']; + // final maxNum = obj['maxNum']; msgtpl.add(RenderChatItem( data: item, child: Ink( @@ -545,16 +745,26 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( + width: 210.0, padding: const EdgeInsets.all(10.0), child: Row( - spacing: 10.0, children: [ - Image.asset( - 'assets/images/hbico.png', - width: 32.0, - fit: BoxFit.contain, + open + ? Icon(Icons.check_circle, size: 32.0, color: Colors.white70) + : Image.asset( + 'assets/images/hbico.png', + width: 32.0, + fit: BoxFit.contain, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + '$remark', + style: const TextStyle(color: Colors.white, fontSize: 14.0), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - Text(item.customElem?.data ?? '', style: const TextStyle(color: Colors.white, fontSize: 14.0)), ], ), ), @@ -564,7 +774,7 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt width: double.infinity, decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.white30, width: .5))), child: const Text( - '拼手气红包', + '红包', style: TextStyle(color: Colors.white70, fontSize: 11.0), ), ), @@ -639,10 +849,6 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt )); } } - // msgtpl.insert( - // 0, - // SizedBox.shrink(), - // ); return msgtpl; } @@ -1517,6 +1723,15 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt ctl.removeNoFriend(conversationID: arguments.value.conversationID); ctl.updateNoFriendMenu(); } + // 跳转到chat地址,销毁当前页面 + Get.offAllNamed( + '/chat', + arguments: arguments.value, + predicate: (route) { + // 清理栈,只保留 `/` + return route.settings.name == '/'; + }, + ); } }, child: const Text('回关', style: TextStyle(color: Colors.white)), diff --git a/lib/pages/chat/index.dart b/lib/pages/chat/index.dart index 2190e23..6437d61 100644 --- a/lib/pages/chat/index.dart +++ b/lib/pages/chat/index.dart @@ -15,6 +15,7 @@ import 'package:loopin/utils/index.dart'; import 'package:loopin/utils/scan_code_type.dart'; // 导入外部枚举 import 'package:loopin/utils/parse_message_summary.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; +import 'package:tencent_cloud_chat_sdk/enum/receive_message_opt.dart'; import '../../behavior/custom_scroll_behavior.dart'; import '../../styles/index.dart'; @@ -394,10 +395,14 @@ class ChatPageState extends State { itemBuilder: (context, index) { // logger.w(chatList[index].conversation.conversationGroupList); // logger.w(chatList[index].isCustomAdmin); + // logger.w(chatList[index].conversation.recvOpt); + final bool quiet = [ReceiveMsgOptType.kTIMRecvMsgOpt_Not_Notify_Except_At].contains(chatList[index].conversation.recvOpt ?? 0) + ? true + : false; // 是否设置了免打扰 final isNoFriend = chatList[index].conversation.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false; final isAdmin = chatList[index].isCustomAdmin != null && (chatList[index].isCustomAdmin?.isNotEmpty ?? false) && chatList[index].isCustomAdmin != '0'; - + final placeholderAsset = chatList[index].conversation.type == 2 ? 'assets/images/group.png' : 'assets/images/avatar/default.png'; // logger.e(chatList[index].isCustomAdmin); return Ink( // color: chatList[index]['topMost'] == null ? Colors.white : Colors.grey[100], //置顶颜色 @@ -415,6 +420,7 @@ class ChatPageState extends State { imageUrl: chatList[index].faceUrl, width: 50, height: 50, + placeholderAsset: placeholderAsset, ), ), @@ -423,17 +429,24 @@ class ChatPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 昵称 Text( chatList[index].conversation.showName ?? '未知', + maxLines: 1, + softWrap: false, + overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: (isAdmin || isNoFriend) ? 20 : 16, fontWeight: (isAdmin || isNoFriend) ? FontWeight.bold : FontWeight.normal), ), const SizedBox(height: 2.0), + // 消息内容 Text( chatList[index].conversation.lastMessage != null ? parseMessageSummary(chatList[index].conversation.lastMessage!) : '', style: const TextStyle(color: Colors.grey, fontSize: 13.0), overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, ), ], ), @@ -456,9 +469,22 @@ class ChatPageState extends State { ), const SizedBox(height: 5.0), // 数字角标 + // class ReceiveMsgOptType { + // 在线正常接收消息,离线时会进行 APNs 推送 + // static const int kTIMRecvMsgOpt_Receive = 0; + // 不会接收到消息,离线不会有推送通知 + // static const int kTIMRecvMsgOpt_Not_Receive = 1; + // 在线正常接收消息,离线不会有推送通知 + // static const int kTIMRecvMsgOpt_Not_Notify = 2; + // 在线接收消息,离线只接收 at 消息的推送 + // static const int kTIMRecvMsgOpt_Not_Notify_Except_At = 3; + // 在线和离线都只接收@消息 + // static const int kTIMRecvMsgOpt_Not_Receive_Except_At = 4; + // } + // 现阶段只允许设置为0和3 Visibility( visible: (chatList[index].conversation.unreadCount ?? 0) > 0, - child: FStyle.badge(chatList[index].conversation.unreadCount ?? 0), + child: FStyle.badge(chatList[index].conversation.unreadCount ?? 0, color: quiet ? Colors.grey : Colors.red), ), ], ), @@ -482,6 +508,9 @@ class ChatPageState extends State { // 跳转陌生人消息页面 logger.e(chatList[index].conversation.conversationGroupList); Get.toNamed('/noFriend'); + } else if (chatList[index].conversation.type == 2) { + // 跳转群聊 type=0非法,1=单聊,2=群聊 + Get.toNamed('/chatGroup', arguments: chatList[index].conversation); } else { // 会话id查询会话详情 Get.toNamed('/chat', arguments: chatList[index].conversation); diff --git a/lib/pages/groupChat/components/invite_action_sheet.dart b/lib/pages/groupChat/components/invite_action_sheet.dart new file mode 100644 index 0000000..dc6d151 --- /dev/null +++ b/lib/pages/groupChat/components/invite_action_sheet.dart @@ -0,0 +1,264 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/styles/index.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; + +class InviteActionSheet extends StatefulWidget { + final String groupID; + + final Function(List selected) onAction; + final String title; + final String actionLabel; + final bool showButton; + + const InviteActionSheet({ + super.key, + required this.groupID, + required this.onAction, + this.title = "", + this.actionLabel = "确认", + this.showButton = true, + }); + + @override + State createState() => _MemberActionSheetState(); +} + +class _MemberActionSheetState extends State { + String _query = ""; + final Set _selectedIDs = {}; // 选中的 userID 集合 + late List members = []; + String nextSeq = ''; + bool hasMore = false; + bool loading = false; + // + @override + void initState() { + super.initState(); + getMemberData(); + } + + //互关好友 + Future getMemberData({bool reset = false}) async { + if (loading) return; + loading = true; + final res = await ImService.instance.getMutualFollowersList( + nextCursor: nextSeq, + ); + if (res.success && res.data != null) { + final userInfoList = res.data!.userFullInfoList ?? []; + final isFinished = res.data!.nextCursor == null || res.data!.nextCursor!.isEmpty; + logger.w('获取成功:${res.data!.nextCursor},是否还有更多:$isFinished'); + if (isFinished) { + hasMore = false; + nextSeq = ''; + } else { + nextSeq = res.data!.nextCursor ?? ''; + hasMore = nextSeq.isNotEmpty ? true : false; + } + + final ids = userInfoList.map((item) => item.userID).whereType().toList(); + if (ids.isNotEmpty) { + getGroupMemberData(userIDs: ids, userList: userInfoList); + } + } else { + logger.e('获取数据失败:${res.desc}'); + } + } + + //群成员 + Future getGroupMemberData({required List userIDs, required List userList}) async { + final res = await ImService.instance.getGroupMembersInfo( + groupID: widget.groupID, + memberList: userIDs, + ); + + if (res.success && res.data != null) { + final List groupMemberList = res.data ?? []; + // 已经在群里的 userID + final inGroupIds = groupMemberList.map((m) => m.userID).whereType().where((id) => id.isNotEmpty).toSet(); + // 过滤掉已经在群里的 + final notInGroupUsers = userList.where((user) => user.userID != null && !inGroupIds.contains(user.userID)).toList(); + setState(() { + members.addAll(notInGroupUsers); + }); + } + setState(() { + loading = false; + }); + } + + String handleText(String? text, String defaultValue) { + if (text == null || text.trim().isEmpty) return defaultValue; + return text; + } + + @override + Widget build(BuildContext context) { + // 搜索过滤 + final filteredMembers = members.where((m) { + final name = m.nickName ?? ''; + return name.contains(_query); + }).toList(); + + return SafeArea( + child: GestureDetector( + behavior: HitTestBehavior.translucent, // 点击空白区域也能触发 + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + centerTitle: true, + forceMaterialTransparency: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container( + color: Colors.grey[300], + height: 1.0, + ), + ), + title: Text( + widget.title, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + }, + ), + ), + body: Column( + children: [ + SizedBox(height: 10), + // 搜索框 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextField( + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + hintText: "搜索", + contentPadding: const EdgeInsets.symmetric(vertical: 8), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + ), + onChanged: (value) { + setState(() { + _query = value.trim(); + }); + }, + ), + ), + + const SizedBox(height: 8), + + // 成员列表 + Expanded( + child: EasyRefresh( + footer: ClassicFooter( + dragText: '加载更多', + armedText: '释放加载', + readyText: '加载中...', + processingText: '加载中...', + processedText: hasMore ? '加载完成' : '没有更多了~', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + onLoad: () async { + // + if (hasMore) { + await getMemberData(); + } + }, + child: ListView.builder( + itemCount: filteredMembers.length, + itemBuilder: (context, index) { + final m = filteredMembers[index]; + final id = m.userID; + final uname = handleText(m.nickName, ''); + final nickname = handleText(m.nickName, '未知昵称'); + final showName = uname.isEmpty ? nickname : uname; + return InkWell( + onTap: () { + setState(() { + if (_selectedIDs.contains(id)) { + _selectedIDs.remove(id); + } else if (id != null && id.isNotEmpty) { + _selectedIDs.add(id); + } + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + children: [ + // 左侧圆形头像 + CircleAvatar( + radius: 20, + backgroundImage: m.faceUrl != null ? NetworkImage(m.faceUrl!) : null, + child: m.faceUrl == null ? const Icon(Icons.person) : null, + ), + const SizedBox(width: 12), + + // 用户名 + Expanded( + child: Text( + showName, + style: const TextStyle(fontSize: 14), + overflow: TextOverflow.ellipsis, + ), + ), + + // 复选框 + Container( + width: 24, + height: 24, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: _selectedIDs.contains(id) ? FStyle.primaryColor : Colors.grey), + color: _selectedIDs.contains(id) ? FStyle.primaryColor : Colors.transparent, + ), + child: _selectedIDs.contains(id) ? const Icon(Icons.check, size: 16, color: Colors.white) : null, + ), + ], + ), + ), + ); + }, + ), + ), + ), + // 底部操作按钮 + if (widget.showButton) + Padding( + padding: const EdgeInsets.all(16), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 48), + backgroundColor: FStyle.primaryColor, + ), + onPressed: _selectedIDs.isEmpty + ? null + : () { + Navigator.pop(context); + widget.onAction(_selectedIDs.toList()); + }, + child: Text( + "${widget.actionLabel}(${_selectedIDs.length})", + style: TextStyle( + color: _selectedIDs.isNotEmpty ? Colors.white : Colors.black, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/groupChat/components/member_action_sheet.dart b/lib/pages/groupChat/components/member_action_sheet.dart new file mode 100644 index 0000000..cacc9cf --- /dev/null +++ b/lib/pages/groupChat/components/member_action_sheet.dart @@ -0,0 +1,302 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/pages/groupChat/controller/group_detail_controller.dart'; +import 'package:loopin/styles/index.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_member_filter_enum.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_search_param.dart'; + +class MemberActionSheet extends StatefulWidget { + final String groupID; + final Function(List selected) onAction; + final String title; + final String actionLabel; + final bool showButton; + final bool showSelf; + + const MemberActionSheet({ + super.key, + required this.groupID, + required this.onAction, + this.title = "", + this.actionLabel = "确认", + this.showButton = true, + this.showSelf = false, + }); + + @override + State createState() => _MemberActionSheetState(); +} + +class _MemberActionSheetState extends State { + String _query = ""; + final Set _selectedIDs = {}; // 选中的 userID 集合 + late List members = []; + late List searchMemberList = []; + String nextSeq = '0'; + String searchCursor = ''; + bool hasMore = false; // 普通列表用 + bool loading = false; + bool isFinished = false; // 搜索列表用 + bool loading2 = false; //搜索 + + // + @override + void initState() { + super.initState(); + if (widget.showSelf) { + final self = Get.find().selfInfo.value!; + members.insert(0, self); + } + getMemberData(); + } + + //群成员 + Future getMemberData() async { + if (loading) return; + loading = true; + final res = await ImService.instance.getGroupMemberList( + groupID: widget.groupID, + filter: GroupMemberFilterTypeEnum.V2TIM_GROUP_MEMBER_FILTER_COMMON, + nextSeq: nextSeq, + count: 100, + ); + if (res.success && res.data != null) { + logger.e(res.data!.nextSeq); + nextSeq = res.data!.nextSeq ?? '0'; + final mem = res.data!.memberInfoList ?? []; + setState(() { + members.addAll(mem); + loading = false; + }); + } + } + + ///搜索群成员 + Future searchMember({bool loadMore = false}) async { + if (loading2) return; + if (_query.isEmpty) { + setState(() { + _query = ''; + }); + return; + } + loading2 = true; + final param = V2TimGroupMemberSearchParam( + keywordList: [_query], + groupIDList: [widget.groupID], + isSearchMemberUserID: true, + isSearchMemberNickName: true, + isSearchMemberRemark: true, + isSearchMemberNameCard: true, + keywordListMatchType: V2TimGroupMemberSearchParam.V2TIM_KEYWORD_LIST_MATCH_TYPE_OR, + searchCount: 100, + searchCursor: searchCursor, + ); + final res = await ImService.instance.searchGroupMembers(param: param); + if (res.success && res.data != null) { + isFinished = res.data!.isFinished ?? true; + final searchCursor = res.data!.nextCursor ?? ''; + final data = res.data!.groupMemberSearchResultItems; + // 搜索结果群的成员 + if (data != null && data[widget.groupID] != null) { + List list = (data[widget.groupID] as List).cast(); + if (!widget.showSelf) { + // 过滤自己 + final self = Get.find().selfInfo.value!; + list = list.where((item) => item.userID != self.userID).toList(); + } + if (loadMore) { + searchMemberList.addAll(list); + } else { + searchMemberList = list; + } + } + logger.w(hasMore); + logger.w(searchCursor); + logger.w(data); + } + setState(() { + loading2 = false; + }); + } + + String handleText(String? text, String defaultValue) { + if (text == null || text.trim().isEmpty) return defaultValue; + return text; + } + + @override + Widget build(BuildContext context) { + List filteredMembers; + if (_query.isEmpty) { + filteredMembers = members; + } else { + filteredMembers = searchMemberList; + } + return SafeArea( + child: GestureDetector( + behavior: HitTestBehavior.translucent, // 点击空白区域也能触发 + onTap: () { + FocusScope.of(context).unfocus(); + }, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + centerTitle: true, + forceMaterialTransparency: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container( + color: Colors.grey[300], + height: 1.0, + ), + ), + title: Text( + widget.title, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.pop(context); + }, + ), + ), + body: Column( + children: [ + SizedBox(height: 10), + // 搜索框 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextField( + decoration: InputDecoration( + prefixIcon: const Icon(Icons.search), + hintText: "搜索成员", + contentPadding: const EdgeInsets.symmetric(vertical: 8), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + ), + onChanged: (value) { + setState(() { + _query = value.trim(); + searchMember(); + }); + }, + ), + ), + + const SizedBox(height: 8), + + // 成员列表 + Expanded( + child: EasyRefresh( + footer: ClassicFooter( + dragText: '加载更多', + armedText: '释放加载', + readyText: '加载中...', + processingText: '加载中...', + processedText: hasMore ? '加载完成' : '没有更多了~', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + onLoad: () async { + // + if (hasMore) { + if (_query.isNotEmpty) { + await searchMember(loadMore: true); + } else { + await getMemberData(); + } + } + }, + child: ListView.builder( + itemCount: filteredMembers.length, + itemBuilder: (context, index) { + final m = filteredMembers[index]; + final id = m.userID; + final uname = handleText(m.nameCard, ''); + final nickname = handleText(m.nickName, '未知昵称'); + final showName = uname.isEmpty ? nickname : uname; + return InkWell( + onTap: () { + setState(() { + if (_selectedIDs.contains(id)) { + _selectedIDs.remove(id); + } else { + _selectedIDs.add(id); + } + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Row( + children: [ + // 左侧圆形头像 + CircleAvatar( + radius: 20, + backgroundImage: m.faceUrl != null ? NetworkImage(m.faceUrl!) : null, + child: m.faceUrl == null ? const Icon(Icons.person) : null, + ), + const SizedBox(width: 12), + + // 用户名 + Expanded( + child: Text( + showName, + style: const TextStyle(fontSize: 14), + overflow: TextOverflow.ellipsis, + ), + ), + if (widget.showButton) + // 复选框 + Container( + width: 24, + height: 24, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: _selectedIDs.contains(id) ? FStyle.primaryColor : Colors.grey), + color: _selectedIDs.contains(id) ? FStyle.primaryColor : Colors.transparent, + ), + child: _selectedIDs.contains(id) ? const Icon(Icons.check, size: 16, color: Colors.white) : null, + ), + ], + ), + ), + ); + }, + ), + ), + ), + // 底部操作按钮 + if (widget.showButton) + Padding( + padding: const EdgeInsets.all(16), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 48), + backgroundColor: FStyle.primaryColor, + ), + onPressed: _selectedIDs.isEmpty + ? null + : () { + Navigator.pop(context); + widget.onAction(_selectedIDs.toList()); + }, + child: Text( + "${widget.actionLabel}(${_selectedIDs.length})", + style: TextStyle( + color: _selectedIDs.isNotEmpty ? Colors.white : Colors.black, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/groupChat/components/set_group_info.dart b/lib/pages/groupChat/components/set_group_info.dart new file mode 100644 index 0000000..77ceace --- /dev/null +++ b/lib/pages/groupChat/components/set_group_info.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class SetGroupInfoPage extends StatelessWidget { + final String appBarTitle; + + /// 标题 + final String fieldLabel; + + /// 字段名 + final int maxLines; + final int maxLength; + final String? initialValue; // 初始值 + final ValueChanged onSubmit; // 提交回调 + + const SetGroupInfoPage({ + super.key, + required this.appBarTitle, + required this.fieldLabel, + this.maxLines = 1, + this.maxLength = 100, + this.initialValue, + required this.onSubmit, + }); + + @override + Widget build(BuildContext context) { + final controller = TextEditingController(text: initialValue); + + return Scaffold( + appBar: AppBar( + title: Text( + appBarTitle, + style: TextStyle(fontSize: 18), + ), + actions: [ + TextButton( + onPressed: () { + if (controller.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('请输入内容')), + ); + return; + } + onSubmit(controller.text.trim()); + Get.back(); + }, + child: const Text("保存", style: TextStyle(color: Colors.black)), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(fieldLabel, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + TextField( + controller: controller, + maxLines: maxLines, + maxLength: maxLength, + decoration: InputDecoration( + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + hintText: "请输入 $fieldLabel", + // counterText: "", // 去掉默认的计数提示 + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/groupChat/controller/group_detail_controller.dart b/lib/pages/groupChat/controller/group_detail_controller.dart new file mode 100644 index 0000000..8c9cdc2 --- /dev/null +++ b/lib/pages/groupChat/controller/group_detail_controller.dart @@ -0,0 +1,148 @@ +import 'package:get/get.dart'; +import 'package:loopin/IM/controller/im_user_info_controller.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/components/my_toast.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_member_filter_enum.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_type.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_full_info.dart'; + +class GroupDetailController extends GetxController { + // 群ID + final String groupID; + GroupDetailController({required this.groupID}); + // 群资料 + Rxn info = Rxn(); + // 群成员列表 + RxList memberList = [].obs; + // 自己在群里的信息 + Rxn selfInfo = Rxn(); + // 是否是群主 + RxBool isOwner = false.obs; + + Future init() async { + await getSelfInfo(); + await getGroupData(); + await getMemberData(); // 最后在拉取成员信息 + canRemoveMembers(); + } + + // 修改群资料 + Future setGroupInfo({required V2TimGroupInfo changedInfo}) async { + if (isOwner.value && info.value != null) { + final res = await ImService.instance.setGroupInfo(info: changedInfo); + if (!res.success) { + // 修改失败重新获取info + MyToast().tip(title: '请稍后再试'); + getGroupData(); + } else { + // info.value?.faceUrl = changedInfo.faceUrl; + final current = info.value!; + if (changedInfo.faceUrl != null) current.faceUrl = changedInfo.faceUrl; + if (changedInfo.groupName != null) current.groupName = changedInfo.groupName; + if (changedInfo.introduction != null) current.introduction = changedInfo.introduction; + if (changedInfo.notification != null) current.notification = changedInfo.notification; + info.refresh(); + } + } + } + + // 获取群资料 + Future getGroupData() async { + final res = await ImService.instance.getGroupsInfo(groupIDList: [groupID]); + if (res.success && res.data != null) { + info.value = res.data!.first.groupInfo ?? V2TimGroupInfo(groupID: groupID, groupType: GroupType.Work); + } + } + + //群成员(这里非群主拿14个,群住拿13个) + Future getMemberData() async { + final res = await ImService.instance.getGroupMemberList( + groupID: groupID, + filter: GroupMemberFilterTypeEnum.V2TIM_GROUP_MEMBER_FILTER_ALL, + nextSeq: "0", + count: isOwner.value ? 13 : 14, + ); + if (res.success && res.data != null) { + memberList.value = res.data!.memberInfoList ?? []; + } + } + + // 自己的群资料 + Future getSelfInfo() async { + final selfID = Get.find().userID.value; + final res = await ImService.instance.getGroupMembersInfo( + groupID: groupID, + memberList: [selfID], + ); + if (res.success && res.data != null) { + selfInfo.value = res.data!.first; + } + } + + // 设置自己的群资料 + Future setSelfInfo({required String nameCard}) async { + final res = await ImService.instance.setGroupMemberInfo( + groupID: groupID, + userID: selfInfo.value?.userID ?? '', + nameCard: nameCard, + ); + if (!res.success) { + MyToast().tip(title: '设置失败', position: 'top'); + } else { + selfInfo.value?.nameCard = nameCard; + selfInfo.refresh(); + } + } + + // 邀请进群 inviteUserToGroup + Future inviteUserToGroup({required List userList}) async { + final res = await ImService.instance.inviteUserToGroup( + groupID: groupID, + userList: userList, + ); + if (res.success && res.data != null) { + refresh(); + } + } + + // 退出群聊 + Future quitGroup() async { + final res = await ImService.instance.quitGroup( + groupID: groupID, + ); + if (res.success) { + refresh(); + } + } + + // 踢人 inviteUserToGroup + Future kickGroupMember(List userIDs) async { + await ImService.instance.kickGroupMember( + groupID: groupID, + memberList: userIDs, + ); + } + + // 踢人权限 + void canRemoveMembers() { + final owner = info.value?.owner; + final myID = selfInfo.value?.userID; + final myRole = selfInfo.value?.role; + + if (owner == null || myID == null || myRole == null) { + isOwner.value = false; + } else if (owner == myID) { + isOwner.value = true; + } else if ([300, 400].contains(myRole)) { + isOwner.value = true; + } else { + isOwner.value = false; + } + // role=200普通 300管理 400群主 + // if (owner == myID) result = true; + // if ([300, 400].contains(myRole)) result = true; + // isOwner + // return false; + } +} diff --git a/lib/pages/groupChat/groupDetail.dart b/lib/pages/groupChat/groupDetail.dart new file mode 100644 index 0000000..c4d8183 --- /dev/null +++ b/lib/pages/groupChat/groupDetail.dart @@ -0,0 +1,541 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:loopin/IM/controller/chat_detail_controller.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/api/common_api.dart'; +import 'package:loopin/components/my_confirm.dart'; +import 'package:loopin/components/my_toast.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/pages/groupChat/components/invite_action_sheet.dart'; +import 'package:loopin/pages/groupChat/components/member_action_sheet.dart'; +import 'package:loopin/pages/groupChat/components/set_group_info.dart'; +import 'package:loopin/pages/groupChat/controller/group_detail_controller.dart'; +import 'package:loopin/service/http.dart'; +import 'package:loopin/utils/index.dart'; +import 'package:loopin/utils/permissions.dart'; +import 'package:shirne_dialog/shirne_dialog.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_type.dart'; +import 'package:tencent_cloud_chat_sdk/enum/receive_message_opt.dart'; +import 'package:tencent_cloud_chat_sdk/enum/receive_message_opt_enum.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart'; +import 'package:wechat_assets_picker/wechat_assets_picker.dart'; + +class Groupdetail extends StatefulWidget { + const Groupdetail({super.key}); + + // final String groupID; + // const Groupdetail({super.key, required this.groupID}); + + @override + State createState() => GroupdetailState(); +} + +class GroupdetailState extends State { + late final GroupDetailController controller; + @override + void initState() { + super.initState(); + // 跳转前先put + controller = Get.find(); + // controller = Get.put(GroupDetailController(groupID: widget.groupID)); + } + + ///设置群头像 + void pickFaceUrl(BuildContext context) async { + final hasPer = await Permissions.requestPhotoPermission(); + if (!hasPer) { + Permissions.showPermissionDialog(); + return; + } + final pickedAssets = await AssetPicker.pickAssets( + context, + pickerConfig: AssetPickerConfig( + textDelegate: const AssetPickerTextDelegate(), + pathNameBuilder: (AssetPathEntity album) { + return Utils.translateAlbumName(album); + }, + maxAssets: 1, + requestType: RequestType.image, + filterOptions: FilterOptionGroup( + imageOption: const FilterOption(), + ), + ), + ); + + if (pickedAssets != null && pickedAssets.isNotEmpty) { + final asset = pickedAssets.first; + final file = await asset.file; // 获取实际文件 + if (file != null) { + final fileSizeInBytes = await file.length(); + final sizeInMB = fileSizeInBytes / (1024 * 1024); + if (sizeInMB > 20) { + MyDialog.toast('图片大小不能超过20MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + } else { + logger.w("图片合法,大小:$sizeInMB MB"); + //走upload(file)上传图片拿到url地址 + final istance = MyDialog.loading('上传中', duration: Duration(minutes: 1)); + final res = await Http.upload(CommonApi.uploadFile, filePath: file.path); + final imgUrl = res['data']['url']; + logger.e(imgUrl); + // 设置群头像 + await controller.setGroupInfo( + changedInfo: V2TimGroupInfo( + groupID: controller.info.value!.groupID, + groupType: GroupType.Work, + faceUrl: imgUrl, + ), + ); + istance.close(); + } + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + centerTitle: true, + forceMaterialTransparency: true, + title: const Text( + "群资料", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Get.back(result: controller.info.value?.groupName ?? ''); + }, + ), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1), + child: Container(height: 1, color: Colors.grey[300]), + ), + ), + body: ListView( + children: [ + // 介绍 + ListTile( + // 群头像 + leading: Obx( + () => GestureDetector( + onTap: () { + // 编辑头像 + if (controller.isOwner.value) { + // + pickFaceUrl(context); + } + }, + child: ClipOval( + child: NetworkOrAssetImage( + imageUrl: controller.info.value?.faceUrl ?? '', + placeholderAsset: 'assets/images/group.png', + height: 60, + width: 60, + ), + ), + ), + ), + // 群名称 + title: Obx( + () => GestureDetector( + onTap: () { + // 去setinfo页 + logger.w('点了名称'); + if (controller.isOwner.value) { + Get.to( + () => SetGroupInfoPage( + appBarTitle: "修改群名称", + fieldLabel: "群名称", + maxLines: 1, + maxLength: 20, + initialValue: controller.info.value?.groupName ?? "", + onSubmit: (value) async { + // 修改群名称 + await controller.setGroupInfo( + changedInfo: V2TimGroupInfo( + groupID: controller.info.value!.groupID, + groupType: GroupType.Work, + groupName: value, + ), + ); + }, + ), + ); + } + }, + child: Text( + Utils.handleText(controller.info.value?.groupName, '', "未命名群聊"), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + // 群简介 + subtitle: Obx( + () => GestureDetector( + onTap: () { + // 去setinfo页 + logger.w('点了简介绍'); + if (controller.isOwner.value) { + Get.to( + () => SetGroupInfoPage( + appBarTitle: "修改群简介", + fieldLabel: "群简介", + maxLines: 5, + maxLength: 100, + initialValue: controller.info.value?.introduction ?? "", + onSubmit: (value) async { + // 修改群名称 + await controller.setGroupInfo( + changedInfo: V2TimGroupInfo( + groupID: controller.info.value!.groupID, + groupType: GroupType.Work, + introduction: value, + ), + ); + }, + ), + ); + } + }, + child: Text( + Utils.handleText(controller.info.value?.introduction, '', "暂无群介绍"), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Colors.grey), + ), + ), + ), + trailing: const Icon(Icons.chevron_right), + ), + const Divider(height: 1), + + // 群成员 + ListTile( + title: Obx( + () => Text("群成员(${controller.info.value?.memberCount ?? 0}/${controller.info.value?.memberMaxCount ?? 0})"), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Obx( + () => Text("查看${controller.info.value?.memberCount ?? 0}个群成员", style: const TextStyle(color: Colors.grey, fontSize: 14)), + ), + const Icon(Icons.chevron_right), + ], + ), + onTap: () { + // 群成员列表 + showModalBottomSheet( + context: context, + barrierColor: Colors.white, + isScrollControlled: true, + useSafeArea: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return MemberActionSheet( + showButton: false, + groupID: controller.groupID, + title: '群成员', + showSelf: true, + onAction: (userIDs) { + // + }, + ); + }, + ); + }, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Wrap( + spacing: 16, + runSpacing: 12, + children: [ + Obx( + () { + return Wrap( + spacing: 16, + runSpacing: 12, + children: controller.memberList.take(8).map((m) { + // 点击成员头像 + return GestureDetector( + onTap: () { + final currentUserID = controller.selfInfo.value?.userID ?? ''; + if (m.userID != currentUserID) { + Get.toNamed('/vloger', arguments: {'memberId': m.userID}); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ClipOval( + child: NetworkOrAssetImage( + imageUrl: m.faceUrl, + height: 50, + width: 50, + ), + ), + const SizedBox(height: 4), + SizedBox( + width: 50, + child: Text( + m.nickName ?? '未知昵称', + maxLines: 1, + softWrap: false, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12), + ), + ), + ], + ), + ); + }).toList(), + ); + }, + ), + // 邀请 + + GestureDetector( + onTap: () { + //检测群人数上限, + logger.w('当前人数${controller.info.value?.memberCount ?? 0}---上限人数${controller.info.value?.memberMaxCount ?? 0}'); + if ((controller.info.value?.memberCount ?? 0) < (controller.info.value?.memberMaxCount ?? 0)) { + // 群人数未达到上限,可以继续加人 + showModalBottomSheet( + context: context, + barrierColor: Colors.white, + isScrollControlled: true, + useSafeArea: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return InviteActionSheet( + groupID: controller.groupID, + title: '邀请新成员', + onAction: (userIDs) { + logger.w("准备添加群成员: $userIDs"); + controller.inviteUserToGroup(userList: userIDs); + }, + ); + }, + ); + } else { + MyToast().tip(title: '群人数已达上限', position: 'top'); + } + }, + child: Column( + children: [ + CircleAvatar( + radius: 25, + child: const Icon(Icons.add), + ), + const SizedBox(height: 4), + const Text("邀请", style: TextStyle(fontSize: 12)), + ], + ), + ), + // 移除 - + Obx( + () => controller.isOwner.value + ? GestureDetector( + onTap: () { + showModalBottomSheet( + context: context, + barrierColor: Colors.white, + isScrollControlled: true, + useSafeArea: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return MemberActionSheet( + groupID: controller.groupID, + title: '移出成员', + onAction: (userIDs) { + logger.w("准备移除群成员: $userIDs"); + controller.kickGroupMember(userIDs); + }, + ); + }, + ); + }, + child: Column( + children: [ + const CircleAvatar( + radius: 25, + child: Icon(Icons.remove), + ), + const SizedBox(height: 4), + const Text("移除", style: TextStyle(fontSize: 12)), + ], + ), + ) + : SizedBox.shrink(), + ) + ], + ), + ), + const Divider(height: 1), + + // 群公告 + ListTile( + title: const Text("群公告"), + subtitle: Obx( + () => Text( + Utils.handleText(controller.info.value?.notification, '', "暂无公告"), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + trailing: const Icon(Icons.chevron_right), + onTap: () { + logger.w('点击了群公告'); + if (controller.isOwner.value) { + Get.to( + () => SetGroupInfoPage( + appBarTitle: "修改群公告", + fieldLabel: "群公告", + maxLines: 8, + maxLength: 200, + initialValue: controller.info.value?.notification ?? "", + onSubmit: (value) async { + // 修改群公告 + await controller.setGroupInfo( + changedInfo: V2TimGroupInfo( + groupID: controller.info.value!.groupID, + groupType: GroupType.Work, + notification: value, + ), + ); + }, + ), + ); + } + }, + ), + const Divider(height: 1), + + // 我的本群昵称 + ListTile( + title: const Text("我的本群昵称"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Obx( + () { + return Text( + Utils.handleText(controller.selfInfo.value?.nameCard, controller.selfInfo.value?.nickName, '未设置昵称'), + style: TextStyle(fontSize: 14), + ); + }, + ), + const Icon(Icons.chevron_right), + ], + ), + onTap: () async { + Get.to( + () => SetGroupInfoPage( + appBarTitle: "修改群昵称", + fieldLabel: "群昵称", + maxLines: 1, + maxLength: 12, + initialValue: controller.selfInfo.value?.nameCard ?? "", + onSubmit: (value) { + // 修改自己的群昵称 + controller.setSelfInfo(nameCard: value); + }, + ), + ); + }, + ), + const Divider(height: 1), + + // 消息免打扰 + Obx( + () => SwitchListTile( + title: Text("消息免打扰"), + value: controller.info.value?.recvOpt == ReceiveMsgOptType.kTIMRecvMsgOpt_Not_Notify_Except_At, // 暂时只提供0和3 + activeColor: Colors.green, + inactiveThumbColor: Colors.grey, + inactiveTrackColor: Colors.white, + onChanged: (v) async { + if (controller.info.value != null) { + logger.w(v); + logger.e(controller.info.value?.recvOpt); + // v=true -> 开启免打扰 = 3 + final res = await ImService.instance.setGroupReceiveMessageOpt( + groupID: controller.groupID, + opt: v ? ReceiveMsgOptEnum.V2TIM_RECEIVE_NOT_NOTIFY_MESSAGE_EXCEPT_AT : ReceiveMsgOptEnum.V2TIM_RECEIVE_MESSAGE, // 关闭免打扰 = 0 + ); + if (res.success) { + controller.info.value!.recvOpt = + v ? ReceiveMsgOptType.kTIMRecvMsgOpt_Not_Notify_Except_At : ReceiveMsgOptType.kTIMRecvMsgOpt_Receive; // 关闭免打扰 = 0 开启=3 + controller.info.refresh(); + } else { + MyToast().tip(title: '网络繁忙,请稍后再试', position: 'top'); + } + } + + // class ReceiveMsgOptType { + // 在线正常接收消息,离线时会进行 APNs 推送 + // static const int kTIMRecvMsgOpt_Receive = 0; + // 不会接收到消息,离线不会有推送通知 + // static const int kTIMRecvMsgOpt_Not_Receive = 1; + // 在线正常接收消息,离线不会有推送通知 + // static const int kTIMRecvMsgOpt_Not_Notify = 2; + // 在线接收消息,离线只接收 at 消息的推送 + // static const int kTIMRecvMsgOpt_Not_Notify_Except_At = 3; + // 在线和离线都只接收@消息 + // static const int kTIMRecvMsgOpt_Not_Receive_Except_At = 4; + // } + }, + ), + ), + const Divider(height: 1), + + // 举报 + // ListTile( + // title: const Text("举报"), + // trailing: const Icon(Icons.chevron_right), + // onTap: () { + // // + // }, + // ), + + // 退出群聊 + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + minimumSize: const Size(double.infinity, 48), + ), + onPressed: () async { + // 二次确认 + final confirmed = await ConfirmDialog.show( + title: "提示", + content: "确认要退出群聊吗?", + ); + + if (confirmed == true) { + await controller.quitGroup(); + Get.find().toolFlag.value = false; // 禁用工具栏 + Get.back(); // 返回上一个页面 + } + }, + child: const Text("退出群聊", style: TextStyle(color: Colors.white)), + ), + ), + const SizedBox(height: 30), + ], + ), + ); + } +} diff --git a/lib/pages/groupChat/groupList.dart b/lib/pages/groupChat/groupList.dart index 55dfd88..13b229d 100644 --- a/lib/pages/groupChat/groupList.dart +++ b/lib/pages/groupChat/groupList.dart @@ -159,6 +159,7 @@ class GrouplistState extends State with SingleTickerProviderStateMixi child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // 群名称 Text( item.groupName?.isNotEmpty == true ? item.groupName! : '群聊', style: const TextStyle( @@ -166,6 +167,7 @@ class GrouplistState extends State with SingleTickerProviderStateMixi fontWeight: FontWeight.normal, ), ), + // 群介绍 if (item.introduction?.isNotEmpty ?? false) ...[ const SizedBox(height: 2.0), Text( diff --git a/lib/pages/groupChat/index.dart b/lib/pages/groupChat/index.dart index 8510d72..03fd9bd 100644 --- a/lib/pages/groupChat/index.dart +++ b/lib/pages/groupChat/index.dart @@ -3,14 +3,15 @@ import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/controller/im_user_info_controller.dart'; +import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/styles/index.dart'; import 'package:tencent_cloud_chat_sdk/enum/group_member_role_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/group_type.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; -import 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart'; class StartGroupChatPage extends StatefulWidget { const StartGroupChatPage({super.key}); @@ -28,6 +29,7 @@ class _StartGroupChatPageState extends State { String page = ''; bool hasMore = true; bool isLoading = true; + bool makeGroup = true; @override void initState() { @@ -41,7 +43,7 @@ class _StartGroupChatPageState extends State { super.dispose(); } - // 分页获取陌关注列表数据 + // 分页获取关注列表数据 Future _loadData({bool reset = false}) async { if (reset) { page = ''; @@ -97,6 +99,13 @@ class _StartGroupChatPageState extends State { /// 创建群聊 void createGroup(Set selectedIds) async { + logger.w(makeGroup); + if (makeGroup == false) { + return; + } + setState(() { + makeGroup = false; + }); // dataList是原始数据List dataList = []; // 通过dataList和selectedIds来构建memberList final ctl = Get.find(); @@ -113,13 +122,42 @@ class _StartGroupChatPageState extends State { ); memberList.insert(0, self); final groupName = buildGroupName(selectedIds); - final res = await ImService.instance.createGroup(groupType: GroupType.Work, groupName: groupName, memberList: memberList); - if (res.success) { - final groupID = res.data; - logger.w(groupID); - final V2TimConversation conv = V2TimConversation(conversationID: 'group_$groupID'); - conv.showName = groupName; - Get.toNamed('/chatGroup', arguments: conv); + try { + final res = await ImService.instance.createGroup(groupType: GroupType.Work, groupName: groupName, memberList: memberList); + if (res.success) { + final groupID = res.data; + logger.w(groupID); + // final V2TimConversation conv = V2TimConversation(conversationID: 'group_$groupID'); + // conv.showName = groupName; + final msgRes = await IMMessage().createTextMessage(text: '加入了群聊:$groupName'); + if (msgRes.success && msgRes.data?.messageInfo != null) { + final groupRes = await IMMessage().sendMessage( + msg: msgRes.data!.messageInfo!, + groupID: groupID, + isExcludedFromUnreadCount: true, + isPush: false, + groupName: groupName, + cloudCustomData: 'tips', + ); + if (groupRes.success) { + final convRes = await ImService.instance.getConversation(conversationID: 'group_$groupID'); + if (convRes.success) { + setState(() { + makeGroup = true; + }); + final V2TimConversation conv = convRes.data; + logger.e(conv.toJson()); + await Get.toNamed('/chatGroup', arguments: conv); + } + } + } + } + } catch (e) { + logger.e(e); + //修改按钮可操作状态 + setState(() { + makeGroup = true; + }); } } @@ -205,29 +243,29 @@ class _StartGroupChatPageState extends State { body: Column( children: [ // 上半部分 - 两个卡片 - Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - _buildCard( - icon: Icons.public, - title: "创建公开群", - onTap: () { - logger.w("跳转到 创建公开群"); - }, - ), - // _buildCard( - // icon: Icons.group, - // title: "创建好友群", - // onTap: () { - // logger.w("跳转到 创建好友群"); - // }, - // ), - ], - ), - ), + // Padding( + // padding: const EdgeInsets.all(8.0), + // child: Column( + // children: [ + // _buildCard( + // icon: Icons.public, + // title: "创建公开群", + // onTap: () { + // logger.w("跳转到 创建公开群"); + // }, + // ), + // _buildCard( + // icon: Icons.group, + // title: "创建好友群", + // onTap: () { + // logger.w("跳转到 创建好友群"); + // }, + // ), + // ], + // ), + // ), - const Divider(), + // const Divider(), // 下半部分 Expanded( @@ -318,11 +356,11 @@ class _StartGroupChatPageState extends State { duration: const Duration(milliseconds: 300), tween: ColorTween( begin: Colors.grey, - end: selectedIds.isEmpty ? Colors.grey : FStyle.primaryColor, + end: (selectedIds.isNotEmpty && makeGroup == true) ? FStyle.primaryColor : Colors.grey, ), builder: (context, bgColor, _) { return ElevatedButton( - onPressed: selectedIds.isEmpty + onPressed: (selectedIds.isEmpty && makeGroup == false) ? null : () { logger.w("选择了用户:$selectedIds"); diff --git a/lib/pages/my/index.dart b/lib/pages/my/index.dart index 13ffb12..3dff36b 100644 --- a/lib/pages/my/index.dart +++ b/lib/pages/my/index.dart @@ -7,6 +7,7 @@ import 'package:loopin/IM/controller/im_user_info_controller.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/api/video_api.dart'; import 'package:loopin/components/custom_sticky_header.dart'; +import 'package:loopin/components/my_confirm.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/components/only_down_scroll_physics.dart'; import 'package:loopin/controller/video_module_controller.dart'; @@ -559,31 +560,40 @@ class MyPageState extends State with SingleTickerProviderStateMixin { onLongPress: () { showModalBottomSheet( context: Get.context!, - backgroundColor: Colors.black.withOpacity(0.8), + backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.lock, color: Colors.white), - title: const Text('设为私密', style: TextStyle(color: Colors.white)), - onTap: () { - Navigator.pop(context); - // TODO: 修改为私密逻辑 - }, - ), - ListTile( - leading: const Icon(Icons.delete, color: Colors.redAccent), - title: const Text('删除视频', style: TextStyle(color: Colors.redAccent)), - onTap: () { - Navigator.pop(context); - // TODO: 删除逻辑 - }, - ), - ], + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ListTile( + // leading: const Icon(Icons.lock, color: Colors.black), + // title: const Text('设为私密', style: TextStyle(color: Colors.black)), + // onTap: () { + // Navigator.pop(context); + // // TODO: 修改为私密逻辑 + // }, + // ), + ListTile( + leading: const Icon(Icons.delete, color: Colors.redAccent), + title: const Text('删除视频', style: TextStyle(color: Colors.redAccent)), + onTap: () async { + Get.back(); + final confirmed = await ConfirmDialog.show( + title: "提示", + content: "确认要删除吗?", + ); + if (confirmed == true) { + Get.back(); + // TODO: 删除逻辑 + } + }, + ), + ], + ), ); }, ); @@ -598,8 +608,9 @@ class MyPageState extends State with SingleTickerProviderStateMixin { /// 视频缩略图 ClipRRect( borderRadius: BorderRadius.circular(12.0), - child: Image.network( - item['cover'] ?? item['firstFrameImg'], + child: NetworkOrAssetImage( + imageUrl: item['cover'] ?? item['firstFrameImg'] ?? '', + placeholderAsset: 'assets/images/bk.jpg', fit: BoxFit.cover, width: double.infinity, height: double.infinity, diff --git a/lib/pages/my/user_info.dart b/lib/pages/my/user_info.dart index dc65f38..b684347 100644 --- a/lib/pages/my/user_info.dart +++ b/lib/pages/my/user_info.dart @@ -9,6 +9,7 @@ import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/service/http.dart'; import 'package:loopin/styles/index.dart'; import 'package:loopin/utils/index.dart'; +import 'package:loopin/utils/permissions.dart'; import 'package:loopin/utils/wxsdk.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; @@ -205,6 +206,11 @@ class _UserInfoState extends State { ///选背景 void pickCover(BuildContext context) async { + final hasPer = await Permissions.requestPhotoPermission(); + if (!hasPer) { + Permissions.showPermissionDialog(); + return; + } final pickedAssets = await AssetPicker.pickAssets( context, pickerConfig: AssetPickerConfig( @@ -226,10 +232,10 @@ class _UserInfoState extends State { if (file != null) { final fileSizeInBytes = await file.length(); final sizeInMB = fileSizeInBytes / (1024 * 1024); - if (sizeInMB > 100) { - MyDialog.toast('图片大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + if (sizeInMB > 200) { + MyDialog.toast('图片大小不能超过200MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); } else { - print("图片合法,大小:$sizeInMB MB"); + print("视频合法,大小:$sizeInMB MB"); //走upload(file)上传图片拿到url地址 final istance = MyDialog.loading('上传中'); final res = await Http.upload(CommonApi.uploadFile, filePath: file.path); @@ -244,6 +250,11 @@ class _UserInfoState extends State { ///选头像 void pickFaceUrl(BuildContext context) async { + final hasPer = await Permissions.requestPhotoPermission(); + if (!hasPer) { + Permissions.showPermissionDialog(); + return; + } final pickedAssets = await AssetPicker.pickAssets( context, pickerConfig: AssetPickerConfig( @@ -265,8 +276,8 @@ class _UserInfoState extends State { if (file != null) { final fileSizeInBytes = await file.length(); final sizeInMB = fileSizeInBytes / (1024 * 1024); - if (sizeInMB > 100) { - MyDialog.toast('图片大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + if (sizeInMB > 20) { + MyDialog.toast('图片大小不能超过20MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); } else { print("图片合法,大小:$sizeInMB MB"); //走upload(file)上传图片拿到url地址 diff --git a/lib/pages/upload_video_page/upload_video_page.dart b/lib/pages/upload_video_page/upload_video_page.dart index 37ade64..1180da7 100644 --- a/lib/pages/upload_video_page/upload_video_page.dart +++ b/lib/pages/upload_video_page/upload_video_page.dart @@ -10,7 +10,9 @@ import 'package:loopin/components/image_viewer.dart'; import 'package:loopin/components/preview_video.dart'; import 'package:loopin/service/http.dart'; import 'package:loopin/utils/index.dart'; +import 'package:loopin/utils/permissions.dart'; import 'package:loopin/utils/snapshot.dart'; +import 'package:shirne_dialog/shirne_dialog.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; class UploadVideoPage extends StatefulWidget { @@ -54,12 +56,10 @@ class _UploadVideoPageState extends State { Future pickVideo() async { descFocusNode.unfocus(); + final hasPer = await Permissions.requestVideoPermission(); - final result = await PhotoManager.requestPermissionExtend(); - - if (!result.isAuth) { - if (!mounted) return; - Get.snackbar('权限被拒绝', '请前往设置授权访问视频'); + if (!hasPer) { + Permissions.showPermissionDialog(); return; } @@ -85,9 +85,20 @@ class _UploadVideoPageState extends State { ); if (pickedAssets != null && pickedAssets.isNotEmpty) { - selectedVideo.value = pickedAssets.first; - status.value = '已选择视频'; - uploadVideo(); + final asset = pickedAssets.first; + final file = await asset.file; // 获取实际文件 + if (file != null) { + final fileSizeInBytes = await file.length(); + final sizeInMB = fileSizeInBytes / (1024 * 1024); + if (sizeInMB > 200) { + MyDialog.toast('文件大小不能超过200MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + } else { + logger.i("文件合法,大小:$sizeInMB MB"); + selectedVideo.value = pickedAssets.first; + status.value = '已选择视频'; + uploadVideo(); + } + } } } @@ -95,11 +106,9 @@ class _UploadVideoPageState extends State { Future pickCoverImage() async { descFocusNode.unfocus(); - final result = await PhotoManager.requestPermissionExtend(); - - if (!result.isAuth) { - if (!mounted) return; - Get.snackbar('权限被拒绝', '请前往设置授权访问照片'); + final hasPer = await Permissions.requestPhotoPermission(); + if (!hasPer) { + Permissions.showPermissionDialog(); return; } @@ -118,9 +127,19 @@ class _UploadVideoPageState extends State { ); if (pickedAssets != null && pickedAssets.isNotEmpty) { - selectedCover.value = pickedAssets.first; - // 执行上传图片逻辑 - uploadImg(); + final asset = pickedAssets.first; + final file = await asset.file; // 获取实际文件 + if (file != null) { + final fileSizeInBytes = await file.length(); + final sizeInMB = fileSizeInBytes / (1024 * 1024); + if (sizeInMB > 20) { + MyDialog.toast('文件大小不能超过20MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + } else { + logger.i("文件合法,大小:$sizeInMB MB"); + selectedCover.value = pickedAssets.first; + uploadImg(); + } + } } } diff --git a/lib/service/http_config.dart b/lib/service/http_config.dart index 8babd8c..9c197bd 100644 --- a/lib/service/http_config.dart +++ b/lib/service/http_config.dart @@ -8,8 +8,8 @@ class HttpConfig { // baseUrl: 'http://43.143.227.203:8099', // baseUrl: 'http://111.62.22.190:8080', // baseUrl: 'http://cjh.wuzhongjie.com.cn', - // baseUrl: 'http://82.156.121.2:8880', - baseUrl: 'http://192.168.1.65:8880', + baseUrl: 'http://82.156.121.2:8880', + // baseUrl: 'http://192.168.1.65:8880', // baseUrl: 'http://192.168.1.22:8080', // connectTimeout: Duration(seconds: 30), diff --git a/lib/utils/index.dart b/lib/utils/index.dart index 05a2fc3..8f279b9 100644 --- a/lib/utils/index.dart +++ b/lib/utils/index.dart @@ -214,4 +214,15 @@ class Utils { return '未知状态'; } } + + // 处理IM的名字 + static String handleText(String? nameCard, String? nickName, String defaultValue) { + if (nameCard != null && nameCard.trim().isNotEmpty) { + return nameCard; + } + if (nickName != null && nickName.trim().isNotEmpty) { + return nickName; + } + return defaultValue; + } } diff --git a/lib/utils/notification_banner.dart b/lib/utils/notification_banner.dart index c453e0c..f4fa00c 100644 --- a/lib/utils/notification_banner.dart +++ b/lib/utils/notification_banner.dart @@ -1,31 +1,45 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/utils/parse_message_summary.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; class NotificationBanner { - static void show(V2TimMessage msg) { - final nickname = msg.nameCard ?? msg.nickName ?? msg.senderProfile?.nickName ?? msg.sender ?? '未知用户'; - final avatar = msg.faceUrl ?? msg.senderProfile?.faceUrl ?? ''; + static void show(V2TimMessage msg, bool isGroup) async { + String name = ''; + String avatar = ''; + if (isGroup) { + final res = await ImService.instance.getGroupsInfo(groupIDList: [msg.groupID!]); + if (res.success && res.data != null) { + V2TimGroupInfo? gpInfo = res.data!.first.groupInfo; + name = gpInfo?.groupName ?? "未知群名"; + avatar = gpInfo?.faceUrl ?? ""; + } else { + name = '获取群名称失败'; + } + } else { + name = msg.nameCard ?? msg.nickName ?? msg.senderProfile?.nickName ?? msg.sender ?? '未知用户'; + avatar = msg.faceUrl ?? msg.senderProfile?.faceUrl ?? ''; + } final text = parseMessageSummary(msg); - Get.snackbar( - nickname, + name, text, duration: const Duration(seconds: 3), snackPosition: SnackPosition.TOP, margin: const EdgeInsets.all(12), backgroundColor: Get.theme.cardColor, colorText: Get.theme.textTheme.bodyLarge?.color, - icon: avatar.isNotEmpty - ? CircleAvatar( - backgroundImage: NetworkImage(avatar), - radius: 16, - ) - : null, + icon: ClipOval( + child: NetworkOrAssetImage( + imageUrl: avatar, + placeholderAsset: isGroup ? 'assets/images/group.png' : 'assets/images/avatar/default.png', + ), + ), onTap: (_) async { // 点击后立刻关闭 Get.closeCurrentSnackbar(); @@ -41,7 +55,7 @@ class NotificationBanner { // 单聊消息 Get.toNamed('/chat', arguments: cRes.data); } else if (msg.groupID != null) { - Get.toNamed('/chat', arguments: cRes.data); + Get.toNamed('/chatGroup', arguments: cRes.data); } } else { // 异常 diff --git a/lib/utils/parse_message_summary.dart b/lib/utils/parse_message_summary.dart index 582466a..ad902f1 100644 --- a/lib/utils/parse_message_summary.dart +++ b/lib/utils/parse_message_summary.dart @@ -1,11 +1,44 @@ import 'dart:convert'; +import 'package:get/get.dart'; +import 'package:loopin/IM/controller/im_user_info_controller.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/models/notify_message.type.dart'; import 'package:loopin/models/summary_type.dart'; +import 'package:loopin/utils/index.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_tips_elem_type.dart'; import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_change_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; +/// 群信息变化类型 +class GroupChangeInfoType { + /// 群自定义字段变更 + static const int custom = 0; + + /// 群名称修改 + static const int name = 1; + + /// 群公告修改 + static const int notification = 3; // 2 文档给出的是2,实际修改返回的3, + + /// 群简介修改 + static const int introduction = 2; // 3 文档给出的3,实际返回的是2 + + /// 群头像修改 + static const int faceUrl = 4; + + /// 群主变更 + static const int owner = 5; + + /// 消息接收选项变更 + static const int receiveMessageOpt = 10; + + /// 全员禁言字段变更 + static const int shutUpAll = 11; +} + String parseMessageSummary(V2TimMessage msg) { switch (msg.elemType) { case MessageElemType.V2TIM_ELEM_TYPE_TEXT: @@ -27,7 +60,93 @@ String parseMessageSummary(V2TimMessage msg) { case MessageElemType.V2TIM_ELEM_TYPE_MERGER: return '[合并转发消息]'; case MessageElemType.V2TIM_ELEM_TYPE_GROUP_TIPS: - return '[群提示]'; + // 群 Tips 通知类型 + + // logger.w(msg.toJson()); + final tipType = msg.groupTipsElem?.type ?? 0; + // logger.e(tipType); + V2TimGroupMemberInfo? op = msg.groupTipsElem?.opMember; + String opName = Utils.handleText(op?.nameCard, op?.nickName, '群主'); + if (op != null) { + opName = op.userID == (Get.find().userID.value) ? '你' : opName; + } + + /// 踢人,邀请,主动退群 + String nickNames = ''; + String kickTips = ''; + String inviteTip = ''; + String quitTips = ''; + + /// 群资料相关的 + String changeInfo = ''; + + /// 群成员信息变更的 + // String memberChangeInfo = ''; + + // 被操作人的信息(踢人,邀请,主动退群,退群后会话将被删除 + List changed = msg.groupTipsElem?.memberList ?? []; + // 踢人,邀请,退群 + if (changed.isNotEmpty) { + final selfID = Get.find().userID.value; + final nicknamesList = changed.map((item) { + if (item == null) return "未知用户"; + if (item.userID == selfID) return "你"; + return Utils.handleText(item.nameCard, item.nickName, "未知用户"); + }).toList(); + nickNames = nicknamesList.join(','); + inviteTip = '$opName 邀请 $nickNames 加入群聊'; + kickTips = '$opName 将 $nickNames 移出群聊'; + quitTips = '$nickNames 退出了群聊'; + } + // 群资料相关的 + List groupChanged = msg.groupTipsElem?.groupChangeInfoList ?? []; + if (groupChanged.isNotEmpty) { + // 在群的设置那里,修改方法都是单条,所以只处理单条就可以了 + for (final item in groupChanged) { + if (item?.type == GroupChangeInfoType.faceUrl) { + // 群头像变更 + changeInfo = '$opName 修改了群头像'; + } else if (item?.type == GroupChangeInfoType.introduction) { + // 群简介 + changeInfo = '$opName 将群简介修改为:${item?.value}'; + } else if (item?.type == GroupChangeInfoType.notification) { + // 群公告 + changeInfo = '$opName 将群公告修改为:${item?.value}'; + } else if (item?.type == GroupChangeInfoType.name) { + // 群名称 + changeInfo = '$opName 将群名称修改为:${item?.value}'; + } else { + changeInfo = '群资料发生变更'; + } + } + } + + // 群成员资料变更 + // List memberChanged = msg.groupTipsElem?.memberChangeInfoList ?? []; + // if (memberChanged.isNotEmpty) { + // for (final item in memberChanged) {} + // } + + if (tipType == GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_INVITE) { + // 加群, + return inviteTip; + } else if (tipType == GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_KICKED) { + // 踢人 + return kickTips; + } else if (tipType == GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_QUIT) { + // 退群 + return quitTips; + } else if (tipType == GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_GROUP_INFO_CHANGE) { + //群资料变更groupName & introduction & notification & faceUrl & owner & custom) + return changeInfo; + } else if (tipType == GroupTipsElemType.V2TIM_GROUP_TIPS_TYPE_MEMBER_INFO_CHANGE) { + //群成员资料变更 (opMember 修改群成员资料:muteTime) + return '群成员资料变更'; + } else { + return '[收到一条群系统通知]'; + } + + case MessageElemType.V2TIM_ELEM_TYPE_GROUP_REPORT: default: return '[未知消息类型1]'; } diff --git a/lib/utils/permissions.dart b/lib/utils/permissions.dart index 6bfd261..1110bc8 100644 --- a/lib/utils/permissions.dart +++ b/lib/utils/permissions.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:get/get.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/components/my_confirm.dart'; import 'package:permission_handler/permission_handler.dart'; class Permissions { @@ -13,9 +15,11 @@ class Permissions { final sdkInt = androidInfo.version.sdkInt; if (sdkInt >= 33) { + // Android 13 及以上 final status = await Permission.videos.request(); return status.isGranted; } else { + // Android 12 及以下 final status = await Permission.storage.request(); return status.isGranted; } @@ -31,17 +35,36 @@ class Permissions { return await _checkAndRequest(Permission.camera); } + // 请求相册权限 + static Future requestPhotoPermission() async { + // return await _checkAndRequest(Permission.photos); + if (Platform.isAndroid) { + final deviceInfoPlugin = DeviceInfoPlugin(); + final androidInfo = await deviceInfoPlugin.androidInfo; + final sdkInt = androidInfo.version.sdkInt; + logger.w(sdkInt); + if (sdkInt >= 33) { + // Android 13 及以上 + final status = await Permission.photos.request(); + return status.isGranted; + } else { + // Android 12 及以下 + final status = await Permission.storage.request(); + return status.isGranted; + } + } else if (Platform.isIOS) { + final status = await Permission.photos.request(); + return status.isGranted; + } + return false; + } + // 请求麦克风权限 static Future requestMicrophonePermission() async { return await _checkAndRequest(Permission.microphone); } - // 请求麦克风权限 - static Future requestPhotoPermission() async { - return await _checkAndRequest(Permission.microphone); - } - - // 请求本地存储权限 + // 请求本地存储权限 static Future requestStoragePermission() async { return await _checkAndRequest(Permission.storage); } @@ -57,8 +80,10 @@ class Permissions { if (result.isGranted) return true; if (result.isPermanentlyDenied) { - _showPermissionDialog(); + // 永久拒绝 只能去设置 + showPermissionDialog(); } else { + // 临时拒绝 提示 Get.snackbar('权限请求失败', '无法访问,请授权对应权限后重试'); } @@ -66,16 +91,14 @@ class Permissions { } // 跳转设置的提示弹窗 - static void _showPermissionDialog() { - Get.defaultDialog( + static void showPermissionDialog() async { + final confirmed = await ConfirmDialog.show( title: '需要权限', - middleText: '请前往系统设置中手动开启权限', - textCancel: '取消', - textConfirm: '去设置', - onConfirm: () async { - Get.back(); - await openAppSettings(); - }, + content: '请前往系统设置中手动开启权限', + confirmText: '去设置', ); + if (confirmed == true) { + await openAppSettings(); + } } }